Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b604cad2fd | ||
|
|
983b47fa76 | ||
|
|
ea065d22a5 | ||
|
|
577d198e1a | ||
|
|
9ead32e9e6 | ||
|
|
993e7c1bc0 | ||
|
|
5b275a17e3 | ||
|
|
0caa327142 | ||
|
|
00d516a59d | ||
|
|
d68b822c76 | ||
|
|
b660d2dd9a | ||
|
|
4d8d75c8a6 | ||
|
|
e6c4ca0731 | ||
|
|
f03405b87a | ||
|
|
ce367e3761 | ||
|
|
cfe65cadca | ||
|
|
68108b8383 | ||
|
|
d2bec1d56e | ||
|
|
c9404f2674 | ||
|
|
80ef19c634 | ||
|
|
83c924566c | ||
|
|
7b58df375e | ||
|
|
1d27bd31d8 | ||
|
|
13d4257c4f | ||
|
|
818ba77693 | ||
|
|
056970e577 | ||
|
|
65ea73e238 | ||
|
|
398f00aceb | ||
|
|
50972713a3 | ||
|
|
fbf3033455 | ||
|
|
cc4d8dabbc | ||
|
|
8bba3d3521 | ||
|
|
be63555e5c | ||
|
|
32a906369f | ||
|
|
064474fd13 | ||
|
|
2ac933b599 | ||
|
|
403fe08bae | ||
|
|
23c7ff7349 | ||
|
|
3169af44cb | ||
|
|
c1d93dbbc6 | ||
|
|
047719cde9 | ||
|
|
13d4b03541 | ||
|
|
f07ae220b0 | ||
|
|
01647445f2 | ||
|
|
13cbf86309 | ||
|
|
792096d209 | ||
|
|
e94de5c9ca | ||
|
|
b3f42624d8 | ||
|
|
d79e4fecf4 | ||
|
|
eb78f959c7 | ||
|
|
51e536c317 | ||
|
|
7af1f6da17 | ||
|
|
c7e310b340 | ||
|
|
23cfa45d89 | ||
|
|
b1346e8c77 | ||
|
|
6e673249dc | ||
|
|
ee801f5d0e | ||
|
|
caf680d999 | ||
|
|
214599a5fd | ||
|
|
9e2285d614 | ||
|
|
7f7e510400 | ||
|
|
d1aa8810f7 | ||
|
|
bfa4b4034c | ||
|
|
8fbde95f92 | ||
|
|
00fb66baa7 | ||
|
|
5fec1e6a58 | ||
|
|
2ec55c6070 | ||
|
|
546bda8514 | ||
|
|
d05847dfe8 | ||
|
|
6551ea719c | ||
|
|
ae57b1b320 | ||
|
|
4d3ad2a337 | ||
|
|
e9c12bb9ff | ||
|
|
42314ab0e3 | ||
|
|
1be53bfa87 | ||
|
|
e8d48376a0 | ||
|
|
0cdb386f56 | ||
|
|
7456fe3fae | ||
|
|
f4df7c3d19 | ||
|
|
544b60f881 | ||
|
|
2eda800a7c | ||
|
|
51de51baa0 | ||
|
|
0d60c2fdf1 | ||
|
|
df8dac0e9b | ||
|
|
8e0c31fbb9 | ||
|
|
2c815b13f0 | ||
|
|
bbd49d7d52 | ||
|
|
8f62da4572 | ||
|
|
0f671a4088 | ||
|
|
108408be81 | ||
|
|
e24b856416 | ||
|
|
612805f030 | ||
|
|
7705dffee0 | ||
|
|
3dceb34174 | ||
|
|
6b747ba891 | ||
|
|
ddd67faeab | ||
|
|
832b57538d | ||
|
|
415bff8e41 | ||
|
|
13c3ddea04 | ||
|
|
b917b30830 | ||
|
|
b34ed5595b | ||
|
|
cbc11155ef | ||
|
|
941f61830f | ||
|
|
5b60cffaae |
4
.claude/commands/new_release.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Evaluate all the updates being made.
|
||||||
|
Update CHANGELOG.md
|
||||||
|
Use the chnages in the git log to determine if its a major, minor or a patch release.
|
||||||
|
Update the package.json first before you push the tag.
|
||||||
@@ -19,6 +19,7 @@ JWT_SECRET=change-this-to-a-secure-random-string-in-production
|
|||||||
# SKIP_FORKS=false
|
# SKIP_FORKS=false
|
||||||
# PRIVATE_REPOSITORIES=false
|
# PRIVATE_REPOSITORIES=false
|
||||||
# MIRROR_ISSUES=false
|
# MIRROR_ISSUES=false
|
||||||
|
# MIRROR_WIKI=false
|
||||||
# MIRROR_STARRED=false
|
# MIRROR_STARRED=false
|
||||||
# MIRROR_ORGANIZATIONS=false
|
# MIRROR_ORGANIZATIONS=false
|
||||||
# PRESERVE_ORG_STRUCTURE=false
|
# PRESERVE_ORG_STRUCTURE=false
|
||||||
|
|||||||
BIN
.github/assets/configuration.png
vendored
|
Before Width: | Height: | Size: 945 KiB After Width: | Height: | Size: 891 KiB |
BIN
.github/assets/logo-no-bg.png
vendored
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
4
.github/workflows/astro-build-test.yml
vendored
@@ -12,6 +12,10 @@ on:
|
|||||||
- 'README.md'
|
- 'README.md'
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-and-test:
|
||||||
name: Build and Test Astro Project
|
name: Build and Test Astro Project
|
||||||
|
|||||||
129
.github/workflows/docker-build.yml
vendored
@@ -1,14 +1,29 @@
|
|||||||
name: Build and Push Docker Images
|
name: Docker Build, Push & Security Scan
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
|
paths:
|
||||||
|
- 'Dockerfile'
|
||||||
|
- '.dockerignore'
|
||||||
|
- 'package.json'
|
||||||
|
- 'bun.lock*'
|
||||||
|
- '.github/workflows/docker-build.yml'
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'Dockerfile'
|
||||||
|
- '.dockerignore'
|
||||||
|
- 'package.json'
|
||||||
|
- 'bun.lock*'
|
||||||
|
- '.github/workflows/docker-build.yml'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # Weekly security scan on Sunday at midnight
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE: ${{ github.repository }}
|
IMAGE: ${{ github.repository }}
|
||||||
|
SHA: ${{ github.event.pull_request.head.sha || github.event.after }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
@@ -17,19 +32,37 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
security-events: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ env.SHA }}
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver-opts: network=host
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
- name: Log into registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Login to Docker Hub for Docker Scout (optional - provides better vulnerability data)
|
||||||
|
# Add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets to enable this
|
||||||
|
- name: Log into Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
# Extract version from tag if present
|
# Extract version from tag if present
|
||||||
- name: Extract version from tag
|
- name: Extract version from tag
|
||||||
id: tag_version
|
id: tag_version
|
||||||
@@ -42,12 +75,88 @@ jobs:
|
|||||||
echo "No version tag, using 'latest'"
|
echo "No version tag, using 'latest'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: docker/build-push-action@v5
|
# Extract metadata for Docker
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.revision=${{ env.SHA }}
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=$repo.default_branch
|
||||||
|
type=semver,pattern=v{{version}}
|
||||||
|
type=sha,prefix=,suffix=,format=short
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=raw,value=${{ steps.tag_version.outputs.VERSION }}
|
||||||
|
|
||||||
|
# Build and push Docker image
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: build-and-push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: |
|
load: ${{ github.event_name == 'pull_request' }}
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
tags: ${{ github.event_name == 'pull_request' && 'gitea-mirror:scan' || steps.meta.outputs.tags }}
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.tag_version.outputs.VERSION }}
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
# Wait for image to be available in registry
|
||||||
|
- name: Wait for image availability
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
run: |
|
||||||
|
echo "Waiting for image to be available in registry..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Docker Scout comprehensive security analysis
|
||||||
|
- name: Docker Scout - Vulnerability Analysis & Recommendations
|
||||||
|
uses: docker/scout-action@v1
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
command: cves,recommendations
|
||||||
|
image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||||
|
sarif-file: scout-results.sarif
|
||||||
|
summary: true
|
||||||
|
exit-code: false
|
||||||
|
only-severities: critical,high
|
||||||
|
write-comment: true
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Docker Scout for Pull Requests (using local image)
|
||||||
|
- name: Docker Scout - Vulnerability Analysis (PR)
|
||||||
|
uses: docker/scout-action@v1
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
command: cves,recommendations
|
||||||
|
image: local://gitea-mirror:scan
|
||||||
|
sarif-file: scout-results.sarif
|
||||||
|
summary: true
|
||||||
|
exit-code: false
|
||||||
|
only-severities: critical,high
|
||||||
|
write-comment: true
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Compare to latest for PRs and pushes
|
||||||
|
- name: Docker Scout - Compare to Latest
|
||||||
|
uses: docker/scout-action@v1
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
with:
|
||||||
|
command: compare
|
||||||
|
image: local://gitea-mirror:scan
|
||||||
|
to: ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
|
||||||
|
ignore-unchanged: true
|
||||||
|
only-severities: critical,high
|
||||||
|
write-comment: true
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Upload security scan results to GitHub Security tab
|
||||||
|
- name: Upload Docker Scout scan results to GitHub Security tab
|
||||||
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
sarif_file: scout-results.sarif
|
||||||
|
|
||||||
|
|||||||
53
.github/workflows/docker-scan.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Docker Security Scan
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
paths:
|
|
||||||
- 'Dockerfile'
|
|
||||||
- '.dockerignore'
|
|
||||||
- 'package.json'
|
|
||||||
- 'bun.lock*'
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
paths:
|
|
||||||
- 'Dockerfile'
|
|
||||||
- '.dockerignore'
|
|
||||||
- 'package.json'
|
|
||||||
- 'bun.lock*'
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * 0' # Run weekly on Sunday at midnight
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
scan:
|
|
||||||
name: Scan Docker Image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
with:
|
|
||||||
driver-opts: network=host
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: false
|
|
||||||
load: true
|
|
||||||
tags: gitea-mirror:scan
|
|
||||||
# Disable GitHub Actions cache for this workflow
|
|
||||||
no-cache: true
|
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
|
||||||
uses: aquasecurity/trivy-action@master
|
|
||||||
with:
|
|
||||||
image-ref: gitea-mirror:scan
|
|
||||||
format: 'table'
|
|
||||||
exit-code: '1'
|
|
||||||
ignore-unfixed: true
|
|
||||||
vuln-type: 'os,library'
|
|
||||||
severity: 'CRITICAL,HIGH'
|
|
||||||
202
CHANGELOG.md
@@ -5,6 +5,208 @@ All notable changes to the Gitea Mirror project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.20.0] - 2025-07-07
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **BREAKING**: Repository moved from `arunavo4/gitea-mirror` to `RayLabsHQ/gitea-mirror`
|
||||||
|
- Docker images now hosted at `ghcr.io/raylabshq/gitea-mirror`
|
||||||
|
- Updated all repository references and links to new organization
|
||||||
|
- License changed from MIT to GNU General Public License v3.0
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Updated GitHub API endpoint for version checking to use new repository location
|
||||||
|
- Corrected all documentation references to point to RayLabsHQ organization
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Removed test security script after confirming vulnerability resolution
|
||||||
|
- Updated base Docker image to version 1.2.18-alpine
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Added repository migration notice in README
|
||||||
|
- Updated quickstart guide with new repository URLs
|
||||||
|
- Updated LXC deployment documentation with new repository location
|
||||||
|
|
||||||
|
## [2.18.0] - 2025-06-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Fourth organization strategy "Mixed Mode" that combines aspects of existing strategies
|
||||||
|
- Personal repositories go to a single configurable organization
|
||||||
|
- Organization repositories preserve their GitHub organization structure
|
||||||
|
- "Override Options" info button in Organization Strategy component explaining customization features
|
||||||
|
- Organization overrides via edit buttons on organization cards
|
||||||
|
- Repository overrides via inline destination editor
|
||||||
|
- Starred repositories behavior and priority hierarchy
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Simplified mixed strategy implementation to reuse existing database fields
|
||||||
|
- Enhanced organization strategy UI with comprehensive override documentation
|
||||||
|
- Better visual indicators for the new mixed strategy with orange color theme
|
||||||
|
|
||||||
|
## [2.17.0] - 2025-06-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Custom destination control for individual repositories with inline editing
|
||||||
|
- Organization-level destination overrides with visual destination editor
|
||||||
|
- Personal repositories organization override configuration option
|
||||||
|
- Visual indicators for starred repositories (⭐ icon) in repository list
|
||||||
|
- Repository-level destination override API endpoint
|
||||||
|
- Destination customization priority hierarchy system
|
||||||
|
- "View on Gitea" buttons for organizations with smart tooltip states
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Enhanced repository table with destination column showing both GitHub org and Gitea destination
|
||||||
|
- Updated organization cards to display custom destinations with visual indicators
|
||||||
|
- Improved getGiteaRepoOwnerAsync to support repository-level destination overrides
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Better visual feedback for custom destinations with badges and inline editing
|
||||||
|
- Enhanced user experience with hover-based edit buttons
|
||||||
|
- Comprehensive destination customization documentation in README
|
||||||
|
|
||||||
|
## [2.16.3] - 2025-06-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Custom 404 error page with helpful navigation links
|
||||||
|
- HoverCard components for better UX in configuration forms
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Replaced popover components with hover cards for information tooltips
|
||||||
|
- Enhanced user experience with responsive hover interactions
|
||||||
|
|
||||||
|
## [2.16.2] - 2025-06-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Bulk actions for repository management with selection support
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Enhanced organization card display with status badges and improved layout
|
||||||
|
|
||||||
|
## [2.16.1] - 2025-06-17
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Improved repository owner handling and mirror strategy in Gitea integration
|
||||||
|
- Updated label for starred repositories organization for consistency
|
||||||
|
|
||||||
|
## [2.16.0] - 2025-06-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Enhanced OrganizationConfiguration component with improved layout and metadata options
|
||||||
|
- New GitHubMirrorSettings component with better organization and flexibility
|
||||||
|
- Enhanced starred repositories content selection and improved layout
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Enhanced configuration interface layout and spacing across multiple components
|
||||||
|
- Streamlined OrganizationStrategy component with cleaner imports and better organization
|
||||||
|
- Improved responsive layout for larger screens in configuration forms
|
||||||
|
- Better icon usage and clarity in configuration components
|
||||||
|
- Enhanced tooltip descriptions and component organization
|
||||||
|
- Improved version comparison logic in health API
|
||||||
|
- Enhanced issue mirroring logic for starred repositories
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed mirror to single organization functionality
|
||||||
|
- Resolved organization strategy layout issues
|
||||||
|
- Cleaned up unused imports across multiple components
|
||||||
|
|
||||||
|
### Refactored
|
||||||
|
- Simplified component structures by removing unused imports and dependencies
|
||||||
|
- Enhanced layout flexibility in GitHubConfigForm and GiteaConfigForm components
|
||||||
|
- Improved component organization and code clarity
|
||||||
|
- Removed ConnectionsForm and useMirror hook for better code organization
|
||||||
|
|
||||||
|
## [2.14.0] - 2025-06-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Enhanced UI components with @radix-ui/react-accordion dependency for improved configuration interface
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Mirror strategies now properly route repositories based on selected strategy
|
||||||
|
- Starred repositories now correctly go to the designated starred repos organization
|
||||||
|
- Organization routing for single-org and flat-user strategies
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Documentation now explains all three mirror strategies (preserve, single-org, flat-user)
|
||||||
|
- Added detailed mirror strategy configuration guide
|
||||||
|
- Updated CLAUDE.md with mirror strategy architecture information
|
||||||
|
- Enhanced Docker Compose development configuration
|
||||||
|
|
||||||
|
## [2.13.2] - 2025-06-15
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Enhanced documentation design and layout
|
||||||
|
- Updated README with improved formatting and content
|
||||||
|
|
||||||
|
## [2.13.1] - 2025-06-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Docker Hub authentication for Docker Scout security scanning
|
||||||
|
- Comprehensive Docker workflow consolidation with build, push & security scan
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Enhanced CI/CD pipeline reliability with better error handling
|
||||||
|
- Updated Bun base image to latest version for improved security
|
||||||
|
- Migrated from Trivy to Docker Scout for more comprehensive security scanning
|
||||||
|
- Enhanced Docker workflow with wait steps for image availability
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Docker Scout action integration issues and image reference problems
|
||||||
|
- Workflow reliability improvements with proper error handling
|
||||||
|
- Security scanning workflow now continues on security issues without failing the build
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated package dependencies to latest versions
|
||||||
|
- Consolidated multiple Docker workflows into single comprehensive workflow
|
||||||
|
- Enhanced security scanning with Docker Scout integration
|
||||||
|
|
||||||
|
## [2.13.0] - 2025-06-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Enhanced Configuration Interface with collapsible components and improved organization strategy UI
|
||||||
|
- Wiki Mirroring Support in configuration settings
|
||||||
|
- Auto-Save Functionality for all config forms, eliminating manual save buttons
|
||||||
|
- Live Refresh functionality with configuration status hooks and enhanced UI components
|
||||||
|
- Enhanced API Config Handling with mapping functions for UI and database structures
|
||||||
|
- Secure Error Responses with createSecureErrorResponse for consistent error handling
|
||||||
|
- Automatic Database Cleanup feature with configuration options and API support
|
||||||
|
- Enhanced Job Recovery with improved database schema and recovery mechanisms
|
||||||
|
- Fork tags to repository UI and enhanced organization cards with repository breakdown
|
||||||
|
- Skeleton loaders and better loading state management across the application
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Navigation context and component loading states across the application
|
||||||
|
- Card components alignment and styling consistency
|
||||||
|
- Error logging and structured error message parsing
|
||||||
|
- HTTP client standardization across the application
|
||||||
|
- Database initialization and management processes
|
||||||
|
- Visual consistency with updated icons and custom logo integration
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Repository mirroring status inconsistencies
|
||||||
|
- Organizations getting stuck on mirroring status when empty
|
||||||
|
- JSON parsing errors and improved error handling
|
||||||
|
- Broken documentation links in README
|
||||||
|
- Various UI contrast and alignment issues
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Migrated testing framework to Bun and updated test configurations
|
||||||
|
- Implemented graceful shutdown and enhanced job recovery capabilities
|
||||||
|
- Replaced SiGitea icons with custom logo
|
||||||
|
- Updated various dependencies for improved stability and performance
|
||||||
|
|
||||||
|
## [2.12.0] - 2025-01-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed SQLite "no such table: mirror_jobs" error during application startup
|
||||||
|
- Implemented automatic database table creation during database initialization
|
||||||
|
- Resolved database schema inconsistencies between development and production environments
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Enhanced database initialization process with automatic table creation and indexing
|
||||||
|
- Added comprehensive error handling for database table creation
|
||||||
|
- Integrated database repair functionality into application startup for better reliability
|
||||||
|
|
||||||
## [2.5.3] - 2025-05-22
|
## [2.5.3] - 2025-05-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
128
CLAUDE.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Gitea Mirror is a web application that automatically mirrors repositories from GitHub to self-hosted Gitea instances. It uses Astro for SSR, React for UI, SQLite for data storage, and Bun as the JavaScript runtime.
|
||||||
|
|
||||||
|
## Essential Commands
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
bun run dev # Start development server (port 3000)
|
||||||
|
bun run build # Build for production
|
||||||
|
bun run preview # Preview production build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
bun test # Run all tests
|
||||||
|
bun test:watch # Run tests in watch mode
|
||||||
|
bun test:coverage # Run tests with coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
```bash
|
||||||
|
bun run init-db # Initialize database
|
||||||
|
bun run reset-users # Reset user accounts (development)
|
||||||
|
bun run cleanup-db # Remove database files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```bash
|
||||||
|
bun run start # Start production server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture & Key Concepts
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
||||||
|
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
||||||
|
- **APIs**: GitHub (Octokit) and Gitea APIs
|
||||||
|
- **Auth**: JWT tokens with bcryptjs password hashing
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
- `/src/pages/api/` - API endpoints (Astro API routes)
|
||||||
|
- `/src/components/` - React components organized by feature
|
||||||
|
- `/src/lib/db/` - Database queries and schema (Drizzle ORM)
|
||||||
|
- `/src/hooks/` - Custom React hooks for data fetching
|
||||||
|
- `/data/` - SQLite database storage location
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
|
||||||
|
1. **API Routes**: All API endpoints follow the pattern `/api/[resource]/[action]` and use `createSecureErrorResponse` for consistent error handling:
|
||||||
|
```typescript
|
||||||
|
import { createSecureErrorResponse } from '@/lib/utils/error-handler';
|
||||||
|
|
||||||
|
export async function POST({ request }: APIContext) {
|
||||||
|
try {
|
||||||
|
// Implementation
|
||||||
|
} catch (error) {
|
||||||
|
return createSecureErrorResponse(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Database Queries**: Located in `/src/lib/db/queries/` organized by domain (users, repositories, etc.)
|
||||||
|
|
||||||
|
3. **Real-time Updates**: Server-Sent Events (SSE) endpoint at `/api/events` for live dashboard updates
|
||||||
|
|
||||||
|
4. **Authentication Flow**:
|
||||||
|
- First user signup creates admin account
|
||||||
|
- JWT tokens stored in cookies
|
||||||
|
- Protected routes check auth via `getUserFromCookie()`
|
||||||
|
|
||||||
|
5. **Mirror Process**:
|
||||||
|
- Discovers repos from GitHub (user/org)
|
||||||
|
- Creates/updates mirror in Gitea
|
||||||
|
- Tracks status in database
|
||||||
|
- Supports scheduled automatic mirroring
|
||||||
|
|
||||||
|
6. **Mirror Strategies**: Three ways to organize repositories in Gitea:
|
||||||
|
- **preserve**: Maintains GitHub structure (default)
|
||||||
|
- **single-org**: All repos go to one organization
|
||||||
|
- **flat-user**: All repos go under user account
|
||||||
|
- Starred repos always go to separate organization (starredReposOrg)
|
||||||
|
- Routing logic in `getGiteaRepoOwner()` function
|
||||||
|
|
||||||
|
### Database Schema (SQLite)
|
||||||
|
- `users` - User accounts and authentication
|
||||||
|
- `configs` - GitHub/Gitea connection settings
|
||||||
|
- `repositories` - Repository mirror status and metadata
|
||||||
|
- `organizations` - Organization structure preservation
|
||||||
|
- `mirror_jobs` - Scheduled mirror operations
|
||||||
|
- `events` - Activity log and notifications
|
||||||
|
|
||||||
|
### Testing Approach
|
||||||
|
- Uses Bun's native test runner (`bun:test`)
|
||||||
|
- Test files use `.test.ts` or `.test.tsx` extension
|
||||||
|
- Setup file at `/src/tests/setup.bun.ts`
|
||||||
|
- Mock utilities available for API testing.
|
||||||
|
|
||||||
|
### Development Tips
|
||||||
|
- Environment variables in `.env` (copy from `.env.example`)
|
||||||
|
- JWT_SECRET auto-generated if not provided
|
||||||
|
- Database auto-initializes on first run
|
||||||
|
- Use `bun run dev:clean` for fresh database start
|
||||||
|
- Tailwind CSS v4 configured with Vite plugin
|
||||||
|
|
||||||
|
### Common Tasks
|
||||||
|
|
||||||
|
**Adding a new API endpoint:**
|
||||||
|
1. Create file in `/src/pages/api/[resource]/[action].ts`
|
||||||
|
2. Use `createSecureErrorResponse` for error handling
|
||||||
|
3. Add corresponding database query in `/src/lib/db/queries/`
|
||||||
|
4. Update types in `/src/types/` if needed
|
||||||
|
|
||||||
|
**Adding a new component:**
|
||||||
|
1. Create in appropriate `/src/components/[feature]/` directory
|
||||||
|
2. Use Shadcn UI components from `/src/components/ui/`
|
||||||
|
3. Follow existing naming patterns (e.g., `RepositoryCard`, `ConfigTabs`)
|
||||||
|
|
||||||
|
**Modifying database schema:**
|
||||||
|
1. Update schema in `/src/lib/db/schema.ts`
|
||||||
|
2. Run `bun run init-db` to recreate database
|
||||||
|
3. Update related queries in `/src/lib/db/queries/`
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# syntax=docker/dockerfile:1.4
|
# syntax=docker/dockerfile:1.4
|
||||||
|
|
||||||
FROM oven/bun:1.2.14-alpine AS base
|
FROM oven/bun:1.2.18-alpine AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite openssl
|
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite openssl
|
||||||
|
|
||||||
|
|||||||
632
LICENSE
@@ -1,21 +1,619 @@
|
|||||||
MIT License
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (c) 2025 ARUNAVO RAY
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Preamble
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
The licenses for most software and other practical works are designed
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
share and change all versions of a program--to make sure it remains free
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
software for all its users.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|||||||
160
README.md
@@ -1,15 +1,25 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src=".github/assets/logo.png" alt="Gitea Mirror Logo" width="120" />
|
<img src=".github/assets/logo-no-bg.png" alt="Gitea Mirror Logo" width="120" />
|
||||||
<h1>Gitea Mirror</h1>
|
<h1>Gitea Mirror</h1>
|
||||||
<p><i>A modern web app for automatically mirroring repositories from GitHub to your self-hosted Gitea.</i></p>
|
<p><i>A modern web app for automatically mirroring repositories from GitHub to your self-hosted Gitea.</i></p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/arunavo4/gitea-mirror/releases/latest"><img src="https://img.shields.io/github/v/tag/arunavo4/gitea-mirror?label=release" alt="release"/></a>
|
<a href="https://github.com/RayLabsHQ/gitea-mirror/releases/latest"><img src="https://img.shields.io/github/v/tag/RayLabsHQ/gitea-mirror?label=release" alt="release"/></a>
|
||||||
<a href="https://github.com/arunavo4/gitea-mirror/actions/workflows/astro-build-test.yml"><img src="https://img.shields.io/github/actions/workflow/status/arunavo4/gitea-mirror/astro-build-test.yml?branch=main" alt="build"/></a>
|
<a href="https://github.com/RayLabsHQ/gitea-mirror/actions/workflows/astro-build-test.yml"><img src="https://img.shields.io/github/actions/workflow/status/RayLabsHQ/gitea-mirror/astro-build-test.yml?branch=main" alt="build"/></a>
|
||||||
<a href="https://github.com/arunavo4/gitea-mirror/pkgs/container/gitea-mirror"><img src="https://img.shields.io/badge/ghcr.io-container-blue?logo=github" alt="container"/></a>
|
<a href="https://github.com/RayLabsHQ/gitea-mirror/pkgs/container/gitea-mirror"><img src="https://img.shields.io/badge/ghcr.io-container-blue?logo=github" alt="container"/></a>
|
||||||
<a href="https://github.com/arunavo4/gitea-mirror/blob/main/LICENSE"><img src="https://img.shields.io/github/license/arunavo4/gitea-mirror" alt="license"/></a>
|
<a href="https://github.com/RayLabsHQ/gitea-mirror/blob/main/LICENSE"><img src="https://img.shields.io/github/license/RayLabsHQ/gitea-mirror" alt="license"/></a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Repository Migration Notice**: Starting from version 2.20, this project has moved from `arunavo4/gitea-mirror` to `RayLabsHQ/gitea-mirror`. Please update your Docker images and Git remotes to use the new location:
|
||||||
|
> ```bash
|
||||||
|
> # Docker
|
||||||
|
> docker pull ghcr.io/raylabshq/gitea-mirror:latest
|
||||||
|
>
|
||||||
|
> # Git remote
|
||||||
|
> git remote set-url origin https://github.com/RayLabsHQ/gitea-mirror.git
|
||||||
|
> ```
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -21,7 +31,7 @@ bun run setup && bun run dev
|
|||||||
|
|
||||||
# Using LXC Containers
|
# Using LXC Containers
|
||||||
# For Proxmox VE (online) - Community script by Tobias ([CrazyWolf13](https://github.com/CrazyWolf13))
|
# For Proxmox VE (online) - Community script by Tobias ([CrazyWolf13](https://github.com/CrazyWolf13))
|
||||||
curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/gitea-mirror-install.sh | bash
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"
|
||||||
|
|
||||||
# For local testing (offline-friendly)
|
# For local testing (offline-friendly)
|
||||||
sudo LOCAL_REPO_DIR=~/Development/gitea-mirror ./scripts/gitea-mirror-lxc-local.sh
|
sudo LOCAL_REPO_DIR=~/Development/gitea-mirror ./scripts/gitea-mirror-lxc-local.sh
|
||||||
@@ -36,9 +46,10 @@ See the [LXC Container Deployment Guide](scripts/README-lxc.md).
|
|||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- 🔁 Sync public, private, or starred GitHub repos to Gitea
|
- 🔁 Sync public, private, or starred GitHub repos to Gitea
|
||||||
- 🏢 Mirror entire organizations with structure preservation
|
- 🏢 Mirror entire organizations with flexible organization strategies
|
||||||
|
- 🎯 Custom destination control for both organizations and individual repositories
|
||||||
- 🐞 Optional mirroring of issues and labels
|
- 🐞 Optional mirroring of issues and labels
|
||||||
- 🌟 Mirror your starred repositories
|
- 🌟 Mirror your starred repositories to a dedicated organization
|
||||||
- 🕹️ Modern user interface with toast notifications and smooth experience
|
- 🕹️ Modern user interface with toast notifications and smooth experience
|
||||||
- 🧠 Smart filtering and job queue with detailed logs
|
- 🧠 Smart filtering and job queue with detailed logs
|
||||||
- 🛠️ Works with personal access tokens (GitHub + Gitea)
|
- 🛠️ Works with personal access tokens (GitHub + Gitea)
|
||||||
@@ -136,12 +147,12 @@ If you want to run the container directly without Docker Compose:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pull the latest multi-architecture image
|
# Pull the latest multi-architecture image
|
||||||
docker pull ghcr.io/arunavo4/gitea-mirror:latest
|
docker pull ghcr.io/RayLabsHQ/gitea-mirror:latest
|
||||||
|
|
||||||
# Run the application with a volume for persistent data
|
# Run the application with a volume for persistent data
|
||||||
docker run -d -p 4321:4321 \
|
docker run -d -p 4321:4321 \
|
||||||
-v gitea-mirror-data:/app/data \
|
-v gitea-mirror-data:/app/data \
|
||||||
ghcr.io/arunavo4/gitea-mirror:latest
|
ghcr.io/RayLabsHQ/gitea-mirror:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Building Docker Images Manually
|
##### Building Docker Images Manually
|
||||||
@@ -167,36 +178,27 @@ docker compose up -d
|
|||||||
|
|
||||||
See [Docker build documentation](./scripts/README-docker.md) for more details.
|
See [Docker build documentation](./scripts/README-docker.md) for more details.
|
||||||
|
|
||||||
##### Using LXC Containers
|
##### Using LXC Containers (Proxmox VE)
|
||||||
|
|
||||||
Gitea Mirror offers two deployment options for LXC containers:
|
For Proxmox VE users, Gitea Mirror can be deployed using the community-maintained script:
|
||||||
|
|
||||||
**1. Proxmox VE (online, recommended for production)**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# One-command installation on Proxmox VE
|
# One-command installation on Proxmox VE
|
||||||
# Uses the community-maintained script by Tobias ([CrazyWolf13](https://github.com/CrazyWolf13))
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"
|
||||||
# at [community-scripts/ProxmoxVED](https://github.com/community-scripts/ProxmoxVED)
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/gitea-mirror-install.sh | bash
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. Local testing (offline-friendly, works on developer laptops)**
|
This community script:
|
||||||
|
- Creates a privileged Alpine Linux LXC container
|
||||||
|
- Installs Bun runtime environment
|
||||||
|
- Clones and builds Gitea Mirror
|
||||||
|
- Configures a systemd service for automatic startup
|
||||||
|
- Sets up the application to run on port 4321
|
||||||
|
|
||||||
```bash
|
> [!NOTE]
|
||||||
# Download the script
|
> The script is maintained by the [Community Scripts for Proxmox VE](https://community-scripts.github.io/ProxmoxVE/) project.
|
||||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
|
> For more information, visit the [Gitea Mirror script documentation](https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror).
|
||||||
chmod +x gitea-mirror-lxc-local.sh
|
|
||||||
|
|
||||||
# Run with your local repo directory
|
After installation, access Gitea Mirror at `http://<container-ip>:4321`
|
||||||
sudo LOCAL_REPO_DIR=~/Development/gitea-mirror ./gitea-mirror-lxc-local.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Both scripts:
|
|
||||||
- Set up a privileged Ubuntu 22.04 LXC container
|
|
||||||
- Install Bun runtime environment
|
|
||||||
- Build the application
|
|
||||||
- Configure a systemd service
|
|
||||||
- Start the service automatically
|
|
||||||
|
|
||||||
The application includes a health check endpoint at `/api/health` for monitoring.
|
The application includes a health check endpoint at `/api/health` for monitoring.
|
||||||
|
|
||||||
@@ -242,7 +244,7 @@ The Docker container can be configured with the following environment variables:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||||
cd gitea-mirror
|
cd gitea-mirror
|
||||||
|
|
||||||
# Quick setup (installs dependencies and initializes the database)
|
# Quick setup (installs dependencies and initializes the database)
|
||||||
@@ -326,6 +328,57 @@ Key configuration options include:
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> **SQLite is the only database required for Gitea Mirror**, handling both data storage and real-time event notifications.
|
> **SQLite is the only database required for Gitea Mirror**, handling both data storage and real-time event notifications.
|
||||||
|
|
||||||
|
### Mirror Strategies & Destination Customization
|
||||||
|
|
||||||
|
Gitea Mirror offers three flexible strategies for organizing your repositories in Gitea, with fine-grained control over destinations:
|
||||||
|
|
||||||
|
#### 1. **Preserve GitHub Structure** (Default)
|
||||||
|
- Personal repositories → Your Gitea username (or custom organization)
|
||||||
|
- Organization repositories → Same organization name in Gitea (with individual overrides)
|
||||||
|
- Maintains the exact structure from GitHub with optional customization
|
||||||
|
|
||||||
|
#### 2. **Single Organization**
|
||||||
|
- All repositories → One designated organization
|
||||||
|
- Simplifies management by consolidating everything
|
||||||
|
- Requires specifying a destination organization name
|
||||||
|
|
||||||
|
#### 3. **Flat User Structure**
|
||||||
|
- All repositories → Your Gitea user account
|
||||||
|
- No organizations needed
|
||||||
|
- Simplest approach for personal use
|
||||||
|
|
||||||
|
#### Destination Customization
|
||||||
|
|
||||||
|
**Organization-Level Overrides:**
|
||||||
|
- Click the edit button on any organization card to set a custom destination
|
||||||
|
- All repositories from that GitHub organization will be mirrored to your specified Gitea organization
|
||||||
|
- Visual indicators show when custom destinations are active
|
||||||
|
|
||||||
|
**Repository-Level Overrides:**
|
||||||
|
- Fine-tune individual repository destinations in the repository table
|
||||||
|
- Click the edit button in the "Destination" column to customize where a specific repo is mirrored
|
||||||
|
- Overrides organization-level settings for maximum flexibility
|
||||||
|
- Starred repositories display a ⭐ icon and always go to the configured starred repos organization
|
||||||
|
|
||||||
|
**Priority Hierarchy:**
|
||||||
|
1. Starred repositories → Always go to `starredReposOrg` (not editable)
|
||||||
|
2. Repository-level custom destination (highest priority for non-starred)
|
||||||
|
3. Organization-level custom destination
|
||||||
|
4. Personal repos override (for non-organization repos)
|
||||||
|
5. Default strategy rules (lowest priority)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **Starred Repositories**: Repositories you've starred on GitHub are automatically organized into a separate organization (default: "starred") and cannot have custom destinations. They're marked with a ⭐ icon for easy identification.
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> **Example Use Cases**:
|
||||||
|
> - Mirror personal repos to `personal-archive` organization
|
||||||
|
> - Redirect `work-org` repos to `company-mirror` in Gitea
|
||||||
|
> - Override a single important repo to go to a special organization
|
||||||
|
> - Keep `company-org` repos in their own `company-org` organization
|
||||||
|
> - Override `community-scripts` to go to `community-mirrors` organization
|
||||||
|
> - This gives you complete control while maintaining GitHub's structure as the default
|
||||||
|
|
||||||
## 🚀 Development
|
## 🚀 Development
|
||||||
|
|
||||||
### Local Development Setup
|
### Local Development Setup
|
||||||
@@ -363,13 +416,13 @@ docker run -d \
|
|||||||
-e USER_UID=1000 \
|
-e USER_UID=1000 \
|
||||||
-e USER_GID=1000 \
|
-e USER_GID=1000 \
|
||||||
-e GITEA__database__DB_TYPE=sqlite3 \
|
-e GITEA__database__DB_TYPE=sqlite3 \
|
||||||
-e GITEA__database__PATH=/data/gitea.db \
|
-e GITEA__database__PATH=/data/gitea/gitea.db \
|
||||||
-e GITEA__server__DOMAIN=localhost \
|
-e GITEA__server__DOMAIN=localhost \
|
||||||
-e GITEA__server__ROOT_URL=http://localhost:3001/ \
|
-e GITEA__server__ROOT_URL=http://localhost:3001/ \
|
||||||
-e GITEA__server__SSH_DOMAIN=localhost \
|
-e GITEA__server__SSH_DOMAIN=localhost \
|
||||||
-e GITEA__server__SSH_PORT=2222 \
|
-e GITEA__server__SSH_PORT=2222 \
|
||||||
-e GITEA__server__START_SSH_SERVER=true \
|
-e GITEA__server__START_SSH_SERVER=true \
|
||||||
-e GITEA__security__INSTALL_LOCK=true \
|
-e GITEA__security__INSTALL_LOCK=false \
|
||||||
-e GITEA__service__DISABLE_REGISTRATION=false \
|
-e GITEA__service__DISABLE_REGISTRATION=false \
|
||||||
gitea/gitea:latest
|
gitea/gitea:latest
|
||||||
```
|
```
|
||||||
@@ -396,7 +449,7 @@ docker run -d \
|
|||||||
-e GITEA_URL=http://gitea:3000 \
|
-e GITEA_URL=http://gitea:3000 \
|
||||||
-e GITEA_TOKEN=your-local-gitea-token \
|
-e GITEA_TOKEN=your-local-gitea-token \
|
||||||
-e GITEA_USERNAME=your-local-gitea-username \
|
-e GITEA_USERNAME=your-local-gitea-username \
|
||||||
arunavo4/gitea-mirror:latest
|
RayLabsHQ/gitea-mirror:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
@@ -497,14 +550,14 @@ Try the following steps:
|
|||||||
> # Run with a volume for persistent data storage
|
> # Run with a volume for persistent data storage
|
||||||
> docker run -d -p 4321:4321 \
|
> docker run -d -p 4321:4321 \
|
||||||
> -v gitea-mirror-data:/app/data \
|
> -v gitea-mirror-data:/app/data \
|
||||||
> ghcr.io/arunavo4/gitea-mirror:latest
|
> ghcr.io/RayLabsHQ/gitea-mirror:latest
|
||||||
> ```
|
> ```
|
||||||
>
|
>
|
||||||
> For homelab/self-hosted setups, you can use the standard Docker Compose file which includes automatic database cleanup:
|
> For homelab/self-hosted setups, you can use the standard Docker Compose file which includes automatic database cleanup:
|
||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
> # Clone the repository
|
> # Clone the repository
|
||||||
> git clone https://github.com/arunavo4/gitea-mirror.git
|
> git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||||
> cd gitea-mirror
|
> cd gitea-mirror
|
||||||
>
|
>
|
||||||
> # Start the application with Docker Compose
|
> # Start the application with Docker Compose
|
||||||
@@ -513,6 +566,36 @@ Try the following steps:
|
|||||||
>
|
>
|
||||||
> This setup provides a complete containerized deployment for the Gitea Mirror application.
|
> This setup provides a complete containerized deployment for the Gitea Mirror application.
|
||||||
|
|
||||||
|
#### Docker Volume Types and Permissions
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Named Volumes vs Bind Mounts**: If you encounter SQLite permission errors even when using Docker, check your volume configuration:
|
||||||
|
|
||||||
|
**✅ Named Volumes (Recommended):**
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- gitea-mirror-data:/app/data # Docker manages permissions automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Bind Mounts (Requires Manual Permission Setup):**
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /host/path/to/data:/app/data # Host filesystem permissions apply
|
||||||
|
```
|
||||||
|
|
||||||
|
**If using bind mounts**, ensure the host directory is owned by UID 1001 (the `gitea-mirror` user):
|
||||||
|
```bash
|
||||||
|
# Set correct ownership for bind mount
|
||||||
|
sudo chown -R 1001:1001 /host/path/to/data
|
||||||
|
sudo chmod -R 755 /host/path/to/data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why named volumes work better:**
|
||||||
|
- Docker automatically handles permissions
|
||||||
|
- Better portability across different hosts
|
||||||
|
- No manual permission setup required
|
||||||
|
- Used by our official docker-compose.yml
|
||||||
|
|
||||||
|
|
||||||
#### Database Maintenance
|
#### Database Maintenance
|
||||||
|
|
||||||
@@ -561,3 +644,4 @@ Try the following steps:
|
|||||||
- [Octokit](https://github.com/octokit/rest.js/) - GitHub REST API client for JavaScript
|
- [Octokit](https://github.com/octokit/rest.js/) - GitHub REST API client for JavaScript
|
||||||
- [Shadcn UI](https://ui.shadcn.com/) - For the beautiful UI components
|
- [Shadcn UI](https://ui.shadcn.com/) - For the beautiful UI components
|
||||||
- [Astro](https://astro.build/) - For the excellent web framework
|
- [Astro](https://astro.build/) - For the excellent web framework
|
||||||
|
- [Community Scripts](https://community-scripts.github.io/ProxmoxVE/) - For the Proxmox VE installation script maintained by [CrazyWolf13](https://github.com/CrazyWolf13)
|
||||||
|
|||||||
425
bun.lock
@@ -4,47 +4,54 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.2.6",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/node": "^9.2.1",
|
"@astrojs/mdx": "^4.3.0",
|
||||||
"@astrojs/react": "^4.2.7",
|
"@astrojs/node": "^9.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@astrojs/react": "^4.3.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.9",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@radix-ui/react-checkbox": "^1.3.1",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-dialog": "^1.1.13",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-popover": "^1.1.13",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-radio-group": "^1.3.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.11",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
"@tanstack/react-virtual": "^3.13.8",
|
"@tanstack/react-virtual": "^3.13.10",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.6",
|
||||||
"astro": "^5.7.13",
|
"astro": "^5.9.3",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.44.2",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.515.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.10",
|
||||||
"tw-animate-css": "^1.3.0",
|
"tw-animate-css": "^1.3.4",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.25.7",
|
"zod": "^3.25.64",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
@@ -52,10 +59,10 @@
|
|||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.20.3",
|
||||||
"vitest": "^3.1.4",
|
"vitest": "^3.2.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -66,10 +73,14 @@
|
|||||||
|
|
||||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
||||||
|
|
||||||
"@astrojs/compiler": ["@astrojs/compiler@2.12.0", "", {}, "sha512-7bCjW6tVDpUurQLeKBUN9tZ5kSv5qYrGmcn0sG0IwacL7isR2ZbyyA3AdZ4uxsuUFOS2SlgReTH7wkxO6zpqWA=="],
|
"@astrojs/check": ["@astrojs/check@0.9.4", "", { "dependencies": { "@astrojs/language-server": "^2.15.0", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "dist/bin.js" } }, "sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA=="],
|
||||||
|
|
||||||
|
"@astrojs/compiler": ["@astrojs/compiler@2.12.2", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="],
|
||||||
|
|
||||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
||||||
|
|
||||||
|
"@astrojs/language-server": ["@astrojs/language-server@2.15.4", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", "@volar/kit": "~2.4.7", "@volar/language-core": "~2.4.7", "@volar/language-server": "~2.4.7", "@volar/language-service": "~2.4.7", "fast-glob": "^3.2.12", "muggle-string": "^0.4.1", "volar-service-css": "0.0.62", "volar-service-emmet": "0.0.62", "volar-service-html": "0.0.62", "volar-service-prettier": "0.0.62", "volar-service-typescript": "0.0.62", "volar-service-typescript-twoslash-queries": "0.0.62", "volar-service-yaml": "0.0.62", "vscode-html-languageservice": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A=="],
|
||||||
|
|
||||||
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
|
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
|
||||||
|
|
||||||
"@astrojs/mdx": ["@astrojs/mdx@4.3.0", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.2", "@mdx-js/mdx": "^3.1.0", "acorn": "^8.14.1", "es-module-lexer": "^1.6.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-OGX2KvPeBzjSSKhkCqrUoDMyzFcjKt5nTE5SFw3RdoLf0nrhyCXBQcCyclzWy1+P+XpOamn+p+hm1EhpCRyPxw=="],
|
"@astrojs/mdx": ["@astrojs/mdx@4.3.0", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.2", "@mdx-js/mdx": "^3.1.0", "acorn": "^8.14.1", "es-module-lexer": "^1.6.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-OGX2KvPeBzjSSKhkCqrUoDMyzFcjKt5nTE5SFw3RdoLf0nrhyCXBQcCyclzWy1+P+XpOamn+p+hm1EhpCRyPxw=="],
|
||||||
@@ -82,11 +93,13 @@
|
|||||||
|
|
||||||
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
||||||
|
|
||||||
|
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.2", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ=="],
|
||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
||||||
|
|
||||||
"@babel/core": ["@babel/core@7.27.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.3", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA=="],
|
"@babel/core": ["@babel/core@7.27.4", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.4", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.4", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g=="],
|
||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.27.3", "", { "dependencies": { "@babel/parser": "^7.27.3", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q=="],
|
"@babel/generator": ["@babel/generator@7.27.3", "", { "dependencies": { "@babel/parser": "^7.27.3", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q=="],
|
||||||
|
|
||||||
@@ -104,9 +117,9 @@
|
|||||||
|
|
||||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.27.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.3" } }, "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg=="],
|
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
"@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
@@ -116,7 +129,7 @@
|
|||||||
|
|
||||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.27.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ=="],
|
"@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
"@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
||||||
|
|
||||||
@@ -132,6 +145,20 @@
|
|||||||
|
|
||||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
||||||
|
|
||||||
|
"@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
|
||||||
|
|
||||||
|
"@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
|
||||||
|
|
||||||
|
"@emmetio/css-parser": ["@emmetio/css-parser@0.4.0", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw=="],
|
||||||
|
|
||||||
|
"@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="],
|
||||||
|
|
||||||
|
"@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="],
|
||||||
|
|
||||||
|
"@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="],
|
||||||
|
|
||||||
|
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||||
@@ -244,27 +271,33 @@
|
|||||||
|
|
||||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.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" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
|
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.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" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
|
||||||
|
|
||||||
"@octokit/auth-token": ["@octokit/auth-token@5.1.2", "", {}, "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
"@octokit/core": ["@octokit/core@6.1.5", "", { "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.2.2", "@octokit/request": "^9.2.3", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "before-after-hook": "^3.0.2", "universal-user-agent": "^7.0.0" } }, "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg=="],
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|
||||||
"@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
|
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||||
|
|
||||||
"@octokit/graphql": ["@octokit/graphql@8.2.2", "", { "dependencies": { "@octokit/request": "^9.2.3", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA=="],
|
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
||||||
|
|
||||||
|
"@octokit/core": ["@octokit/core@7.0.2", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g=="],
|
||||||
|
|
||||||
|
"@octokit/endpoint": ["@octokit/endpoint@11.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ=="],
|
||||||
|
|
||||||
|
"@octokit/graphql": ["@octokit/graphql@9.0.1", "", { "dependencies": { "@octokit/request": "^10.0.2", "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg=="],
|
||||||
|
|
||||||
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
|
||||||
|
|
||||||
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@11.6.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw=="],
|
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.0.1", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-m1KvHlueScy4mQJWvFDCxFBTIdXS0K1SgFGLmqHyX90mZdCIv6gWBbKRhatxRjhGlONuTK/hztYdaqrTXcFZdQ=="],
|
||||||
|
|
||||||
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@5.3.1", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw=="],
|
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
|
||||||
|
|
||||||
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@13.5.0", "", { "dependencies": { "@octokit/types": "^13.10.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw=="],
|
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.0.0", "", { "dependencies": { "@octokit/types": "^14.1.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g=="],
|
||||||
|
|
||||||
"@octokit/request": ["@octokit/request@9.2.3", "", { "dependencies": { "@octokit/endpoint": "^10.1.4", "@octokit/request-error": "^6.1.8", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^2.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w=="],
|
"@octokit/request": ["@octokit/request@10.0.2", "", { "dependencies": { "@octokit/endpoint": "^11.0.0", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA=="],
|
||||||
|
|
||||||
"@octokit/request-error": ["@octokit/request-error@6.1.8", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ=="],
|
"@octokit/request-error": ["@octokit/request-error@7.0.0", "", { "dependencies": { "@octokit/types": "^14.0.0" } }, "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg=="],
|
||||||
|
|
||||||
"@octokit/rest": ["@octokit/rest@21.1.1", "", { "dependencies": { "@octokit/core": "^6.1.4", "@octokit/plugin-paginate-rest": "^11.4.2", "@octokit/plugin-request-log": "^5.3.1", "@octokit/plugin-rest-endpoint-methods": "^13.3.0" } }, "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg=="],
|
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
|
||||||
|
|
||||||
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
|
||||||
|
|
||||||
@@ -274,12 +307,16 @@
|
|||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||||
|
|
||||||
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
|
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
|
||||||
|
|
||||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA=="],
|
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||||
@@ -298,6 +335,8 @@
|
|||||||
|
|
||||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||||
@@ -318,8 +357,12 @@
|
|||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.9", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YSjEfBXnhUELsO2VzjdtYYD4CfQjvao+lhhrX5XsHD7/cyUNzljF1FHEbgTPN7LH2MClfwRMIsYlqTYpKTTe2A=="],
|
||||||
|
|
||||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
|
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ=="],
|
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ=="],
|
||||||
@@ -350,7 +393,7 @@
|
|||||||
|
|
||||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.11", "", {}, "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag=="],
|
||||||
|
|
||||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
|
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
|
||||||
|
|
||||||
@@ -410,39 +453,39 @@
|
|||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.7", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.7" } }, "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.10", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.10" } }, "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.7", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.7", "@tailwindcss/oxide-darwin-arm64": "4.1.7", "@tailwindcss/oxide-darwin-x64": "4.1.7", "@tailwindcss/oxide-freebsd-x64": "4.1.7", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", "@tailwindcss/oxide-linux-x64-musl": "4.1.7", "@tailwindcss/oxide-wasm32-wasi": "4.1.7", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" } }, "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.10", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.10", "@tailwindcss/oxide-darwin-arm64": "4.1.10", "@tailwindcss/oxide-darwin-x64": "4.1.10", "@tailwindcss/oxide-freebsd-x64": "4.1.10", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", "@tailwindcss/oxide-linux-x64-musl": "4.1.10", "@tailwindcss/oxide-wasm32-wasi": "4.1.10", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" } }, "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.7", "", { "os": "android", "cpu": "arm64" }, "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.10", "", { "os": "android", "cpu": "arm64" }, "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7", "", { "os": "linux", "cpu": "arm" }, "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g=="],
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10", "", { "os": "linux", "cpu": "arm" }, "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA=="],
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A=="],
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg=="],
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.10", "", { "os": "linux", "cpu": "x64" }, "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA=="],
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.10", "", { "os": "linux", "cpu": "x64" }, "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.7", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.9", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A=="],
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.10", "", { "os": "win32", "cpu": "x64" }, "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA=="],
|
||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.7", "", { "dependencies": { "@tailwindcss/node": "4.1.7", "@tailwindcss/oxide": "4.1.7", "tailwindcss": "4.1.7" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ=="],
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.10", "", { "dependencies": { "@tailwindcss/node": "4.1.10", "@tailwindcss/oxide": "4.1.10", "tailwindcss": "4.1.10" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A=="],
|
||||||
|
|
||||||
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.9", "", { "dependencies": { "@tanstack/virtual-core": "3.13.9" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA=="],
|
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.10", "", { "dependencies": { "@tanstack/virtual-core": "3.13.10" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-nvrzk4E9mWB4124YdJ7/yzwou7IfHxlSef6ugCFcBfRmsnsma3heciiiV97sBNxyc3VuwtZvmwXd0aB5BpucVw=="],
|
||||||
|
|
||||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.9", "", {}, "sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ=="],
|
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.10", "", {}, "sha512-sPEDhXREou5HyZYqSWIqdU580rsF6FGeN7vpzijmP3KTiOGjOMZASz4Y6+QKjiFQwhWrR58OP8izYaNGVxvViA=="],
|
||||||
|
|
||||||
"@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="],
|
"@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="],
|
||||||
|
|
||||||
@@ -464,8 +507,12 @@
|
|||||||
|
|
||||||
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
||||||
|
|
||||||
|
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||||
|
|
||||||
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||||
|
|
||||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||||
@@ -486,9 +533,9 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
|
"@types/node": ["@types/node@22.15.23", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.1.6", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q=="],
|
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.1.5", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg=="],
|
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
|
||||||
|
|
||||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
@@ -496,21 +543,37 @@
|
|||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.5.0", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.5.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.11", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@3.1.4", "", { "dependencies": { "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA=="],
|
"@vitest/expect": ["@vitest/expect@3.2.3", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.3", "@vitest/utils": "3.2.3", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ=="],
|
||||||
|
|
||||||
"@vitest/mocker": ["@vitest/mocker@3.1.4", "", { "dependencies": { "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA=="],
|
"@vitest/mocker": ["@vitest/mocker@3.2.3", "", { "dependencies": { "@vitest/spy": "3.2.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA=="],
|
||||||
|
|
||||||
"@vitest/pretty-format": ["@vitest/pretty-format@3.1.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg=="],
|
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.3", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng=="],
|
||||||
|
|
||||||
"@vitest/runner": ["@vitest/runner@3.1.4", "", { "dependencies": { "@vitest/utils": "3.1.4", "pathe": "^2.0.3" } }, "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ=="],
|
"@vitest/runner": ["@vitest/runner@3.2.3", "", { "dependencies": { "@vitest/utils": "3.2.3", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w=="],
|
||||||
|
|
||||||
"@vitest/snapshot": ["@vitest/snapshot@3.1.4", "", { "dependencies": { "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg=="],
|
"@vitest/snapshot": ["@vitest/snapshot@3.2.3", "", { "dependencies": { "@vitest/pretty-format": "3.2.3", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA=="],
|
||||||
|
|
||||||
"@vitest/spy": ["@vitest/spy@3.1.4", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg=="],
|
"@vitest/spy": ["@vitest/spy@3.2.3", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw=="],
|
||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@3.1.4", "", { "dependencies": { "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg=="],
|
"@vitest/utils": ["@vitest/utils@3.2.3", "", { "dependencies": { "@vitest/pretty-format": "3.2.3", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q=="],
|
||||||
|
|
||||||
|
"@volar/kit": ["@volar/kit@2.4.14", "", { "dependencies": { "@volar/language-service": "2.4.14", "@volar/typescript": "2.4.14", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-kBcmHjEodtmYGJELHePZd2JdeYm4ZGOd9F/pQ1YETYIzAwy4Z491EkJ1nRSo/GTxwKt0XYwYA/dHSEgXecVHRA=="],
|
||||||
|
|
||||||
|
"@volar/language-core": ["@volar/language-core@2.4.14", "", { "dependencies": { "@volar/source-map": "2.4.14" } }, "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w=="],
|
||||||
|
|
||||||
|
"@volar/language-server": ["@volar/language-server@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "@volar/language-service": "2.4.14", "@volar/typescript": "2.4.14", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-P3mGbQbW0v40UYBnb3DAaNtRYx6/MGOVKzdOWmBCGwjUkCR2xBkGrCFt05XnPDwFS/cTWDh2U6Mc9lpZ8Aecfw=="],
|
||||||
|
|
||||||
|
"@volar/language-service": ["@volar/language-service@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-vNC3823EJohdzLTyjZoCMPwoWCfINB5emusniCkW5CGoGHQov4VVmT6yI5ncgP/NpgAIUv2NEkJooXvLHA4VeQ=="],
|
||||||
|
|
||||||
|
"@volar/source-map": ["@volar/source-map@2.4.14", "", {}, "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ=="],
|
||||||
|
|
||||||
|
"@volar/typescript": ["@volar/typescript@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw=="],
|
||||||
|
|
||||||
|
"@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="],
|
||||||
|
|
||||||
|
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||||
|
|
||||||
@@ -518,6 +581,8 @@
|
|||||||
|
|
||||||
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
|
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
|
|
||||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
@@ -538,7 +603,7 @@
|
|||||||
|
|
||||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||||
|
|
||||||
"astro": ["astro@5.8.0", "", { "dependencies": { "@astrojs/compiler": "^2.11.0", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.2", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-G57ELkdIntDiSrucA5lQaRtBOjquaZ9b9NIwoz2f471ZuuJcynLjWgItgBzlrz5UMY4WqnFbVWUCKlJb7nt9bA=="],
|
"astro": ["astro@5.9.3", "", { "dependencies": { "@astrojs/compiler": "^2.12.2", "@astrojs/internal-helpers": "0.6.1", "@astrojs/markdown-remark": "6.3.2", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^2.4.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.1", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.2.0", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.0.2", "cssesc": "^3.0.0", "debug": "^4.4.0", "deterministic-object-hash": "^2.0.2", "devalue": "^5.1.1", "diff": "^5.2.0", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.6.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.3.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.1.1", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.0", "package-manager-detector": "^1.1.0", "picomatch": "^4.0.2", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.1", "shiki": "^3.2.1", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.12", "tsconfck": "^3.1.5", "ultrahtml": "^1.6.0", "unifont": "~0.5.0", "unist-util-visit": "^5.0.0", "unstorage": "^1.15.0", "vfile": "^6.0.3", "vite": "^6.3.4", "vitefu": "^1.0.6", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.1", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.5", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.33.3" }, "bin": { "astro": "astro.js" } }, "sha512-VReZrpUa/3rfeiVvsQ1A2M3ujDPI+pDGIYOMtXPEZwut8tZoEyealXXLjitgCsJ+3dunKGZbg4Eak6i+r0vniw=="],
|
||||||
|
|
||||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
@@ -550,12 +615,14 @@
|
|||||||
|
|
||||||
"bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="],
|
"bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="],
|
||||||
|
|
||||||
"before-after-hook": ["before-after-hook@3.0.2", "", {}, "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A=="],
|
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||||
|
|
||||||
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
|
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
|
||||||
|
|
||||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
|
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
||||||
@@ -596,6 +663,8 @@
|
|||||||
|
|
||||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||||
|
|
||||||
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
@@ -672,7 +741,7 @@
|
|||||||
|
|
||||||
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
"dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.43.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA=="],
|
"drizzle-orm": ["drizzle-orm@0.44.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="],
|
||||||
|
|
||||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||||
|
|
||||||
@@ -682,7 +751,9 @@
|
|||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.159", "", {}, "sha512-CEvHptWAMV5p6GJ0Lq8aheyvVbfzVrv5mmidu1D3pidoVNkB3tTBsTMVtPJ+rzRK5oV229mCLz9Zj/hNvU8GBA=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.159", "", {}, "sha512-CEvHptWAMV5p6GJ0Lq8aheyvVbfzVrv5mmidu1D3pidoVNkB3tTBsTMVtPJ+rzRK5oV229mCLz9Zj/hNvU8GBA=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
"emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
@@ -726,12 +797,20 @@
|
|||||||
|
|
||||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||||
|
|
||||||
"fast-content-type-parse": ["fast-content-type-parse@2.0.1", "", {}, "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q=="],
|
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
||||||
|
|
||||||
|
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
||||||
|
|
||||||
"fontace": ["fontace@0.3.0", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg=="],
|
"fontace": ["fontace@0.3.0", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg=="],
|
||||||
@@ -746,6 +825,8 @@
|
|||||||
|
|
||||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||||
|
|
||||||
|
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||||
|
|
||||||
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||||
|
|
||||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
@@ -754,6 +835,8 @@
|
|||||||
|
|
||||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
@@ -822,12 +905,18 @@
|
|||||||
|
|
||||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||||
|
|
||||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||||
|
|
||||||
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
||||||
@@ -844,8 +933,12 @@
|
|||||||
|
|
||||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
||||||
|
|
||||||
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
||||||
|
|
||||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||||
@@ -898,7 +991,7 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="],
|
"lucide-react": ["lucide-react@0.515.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw=="],
|
||||||
|
|
||||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||||
|
|
||||||
@@ -946,6 +1039,8 @@
|
|||||||
|
|
||||||
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||||
|
|
||||||
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
"micromark": ["micromark@4.0.2", "", { "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" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
"micromark": ["micromark@4.0.2", "", { "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" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||||
|
|
||||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "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" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "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" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||||
@@ -1016,6 +1111,8 @@
|
|||||||
|
|
||||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||||
|
|
||||||
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||||
@@ -1032,6 +1129,8 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
||||||
@@ -1078,6 +1177,8 @@
|
|||||||
|
|
||||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||||
|
|
||||||
|
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||||
|
|
||||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
"pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="],
|
"pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="],
|
||||||
@@ -1088,6 +1189,8 @@
|
|||||||
|
|
||||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
|
||||||
|
|
||||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||||
|
|
||||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||||
@@ -1098,6 +1201,8 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||||
|
|
||||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
@@ -1158,6 +1263,12 @@
|
|||||||
|
|
||||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||||
|
|
||||||
|
"request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
|
||||||
|
|
||||||
|
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
|
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
|
||||||
@@ -1170,10 +1281,14 @@
|
|||||||
|
|
||||||
"retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
|
"retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
|
||||||
|
|
||||||
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
|
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
|
||||||
|
|
||||||
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
|
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
|
||||||
|
|
||||||
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
@@ -1202,7 +1317,7 @@
|
|||||||
|
|
||||||
"smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="],
|
"smol-toml": ["smol-toml@1.3.4", "", {}, "sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA=="],
|
||||||
|
|
||||||
"sonner": ["sonner@2.0.3", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA=="],
|
"sonner": ["sonner@2.0.5", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
|
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
|
||||||
|
|
||||||
@@ -1216,14 +1331,16 @@
|
|||||||
|
|
||||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||||
|
|
||||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||||
|
|
||||||
|
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
||||||
|
|
||||||
"style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="],
|
"style-to-js": ["style-to-js@1.1.16", "", { "dependencies": { "style-to-object": "1.0.8" } }, "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
||||||
@@ -1232,9 +1349,9 @@
|
|||||||
|
|
||||||
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="],
|
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.1.7", "", {}, "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg=="],
|
"tailwindcss": ["tailwindcss@4.1.10", "", {}, "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||||
|
|
||||||
@@ -1248,16 +1365,18 @@
|
|||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||||
|
|
||||||
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
|
"tinypool": ["tinypool@1.1.0", "", {}, "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ=="],
|
||||||
|
|
||||||
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
||||||
|
|
||||||
"tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="],
|
"tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="],
|
||||||
|
|
||||||
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
|
"tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
|
||||||
|
|
||||||
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
|
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
|
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
|
||||||
@@ -1272,14 +1391,18 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tsx": ["tsx@4.19.4", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q=="],
|
"tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="],
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.3.0", "", {}, "sha512-jrJ0XenzS9KVuDThJDvnhalbl4IYiMQ/XvpA0a2FL8KmlK+6CSMviO7ROY/I7z1NnUs5NnDhlM6fXmF40xPxzw=="],
|
"tw-animate-css": ["tw-animate-css@1.3.4", "", {}, "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||||
|
|
||||||
|
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||||
|
|
||||||
|
"typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
|
||||||
|
|
||||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||||
|
|
||||||
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
|
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
|
||||||
@@ -1338,11 +1461,45 @@
|
|||||||
|
|
||||||
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
|
||||||
|
|
||||||
"vite-node": ["vite-node@3.1.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA=="],
|
"vite-node": ["vite-node@3.2.3", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ=="],
|
||||||
|
|
||||||
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
|
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
|
||||||
|
|
||||||
"vitest": ["vitest@3.1.4", "", { "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", "@vitest/pretty-format": "^3.1.4", "@vitest/runner": "3.1.4", "@vitest/snapshot": "3.1.4", "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.13", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.1.4", "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ=="],
|
"vitest": ["vitest@3.2.3", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.3", "@vitest/mocker": "3.2.3", "@vitest/pretty-format": "^3.2.3", "@vitest/runner": "3.2.3", "@vitest/snapshot": "3.2.3", "@vitest/spy": "3.2.3", "@vitest/utils": "3.2.3", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.0", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.3", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.3", "@vitest/ui": "3.2.3", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww=="],
|
||||||
|
|
||||||
|
"volar-service-css": ["volar-service-css@0.0.62", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg=="],
|
||||||
|
|
||||||
|
"volar-service-emmet": ["volar-service-emmet@0.0.62", "", { "dependencies": { "@emmetio/css-parser": "^0.4.0", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ=="],
|
||||||
|
|
||||||
|
"volar-service-html": ["volar-service-html@0.0.62", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ=="],
|
||||||
|
|
||||||
|
"volar-service-prettier": ["volar-service-prettier@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w=="],
|
||||||
|
|
||||||
|
"volar-service-typescript": ["volar-service-typescript@0.0.62", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.3", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g=="],
|
||||||
|
|
||||||
|
"volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng=="],
|
||||||
|
|
||||||
|
"volar-service-yaml": ["volar-service-yaml@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.15.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig=="],
|
||||||
|
|
||||||
|
"vscode-css-languageservice": ["vscode-css-languageservice@6.3.6", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-fU4h8mT3KlvfRcbF74v/M+Gzbligav6QMx4AD/7CbclWPYOpGb9kgIswfpZVJbIcOEJJACI9iYizkNwdiAqlHw=="],
|
||||||
|
|
||||||
|
"vscode-html-languageservice": ["vscode-html-languageservice@5.5.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-No6Er2P2L8IsXDnUFlp0bP4f2sdkJv+zJLZYFhtEQIp+2xNfxY8WYkhSxLJ/7bZhuV/aU55lmGSSHBVxSGer3Q=="],
|
||||||
|
|
||||||
|
"vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="],
|
||||||
|
|
||||||
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||||
|
|
||||||
|
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
||||||
|
|
||||||
|
"vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="],
|
||||||
|
|
||||||
|
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||||
|
|
||||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||||
|
|
||||||
@@ -1372,8 +1529,16 @@
|
|||||||
|
|
||||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||||
|
|
||||||
|
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||||
|
|
||||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
|
||||||
|
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
||||||
|
|
||||||
|
"yaml-language-server": ["yaml-language-server@1.15.0", "", { "dependencies": { "ajv": "^8.11.0", "lodash": "4.17.21", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^7.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2", "yaml": "2.2.2" }, "optionalDependencies": { "prettier": "2.8.7" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw=="],
|
||||||
|
|
||||||
|
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||||
|
|
||||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
|
"yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
|
||||||
@@ -1382,7 +1547,7 @@
|
|||||||
|
|
||||||
"yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
|
"yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
|
||||||
|
|
||||||
"zod": ["zod@3.25.32", "", {}, "sha512-OSm2xTIRfW8CV5/QKgngwmQW/8aPfGdaQFlrGoErlgg/Epm7cjb6K6VEyExfe65a3VybUOnu381edLb0dfJl0g=="],
|
"zod": ["zod@3.25.64", "", {}, "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g=="],
|
||||||
|
|
||||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
|
||||||
|
|
||||||
@@ -1390,15 +1555,23 @@
|
|||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@4.5.0", "", { "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg=="],
|
||||||
|
|
||||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"@babel/generator/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
"@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ=="],
|
||||||
|
|
||||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
"@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.27.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ=="],
|
||||||
|
|
||||||
|
"@babel/helpers/@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="],
|
||||||
|
|
||||||
|
"@babel/template/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||||
|
|
||||||
@@ -1420,14 +1593,24 @@
|
|||||||
|
|
||||||
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||||
|
|
||||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"@types/babel__core/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
|
"@types/babel__template/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"boxen/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
"boxen/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||||
|
|
||||||
|
"boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
|
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
"hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
|
"hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
|
||||||
|
|
||||||
|
"magicast/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
@@ -1436,22 +1619,66 @@
|
|||||||
|
|
||||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||||
|
|
||||||
"strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||||
|
|
||||||
|
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||||
|
|
||||||
|
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||||
|
|
||||||
|
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
|
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
||||||
|
|
||||||
|
"yaml-language-server/vscode-languageserver": ["vscode-languageserver@7.0.0", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],
|
||||||
|
|
||||||
|
"yaml-language-server/yaml": ["yaml@2.2.2", "", {}, "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core": ["@babel/core@7.27.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.3", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
"@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||||
|
|
||||||
|
"widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||||
|
|
||||||
|
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||||
|
|
||||||
|
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
|
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.16.0", "", { "dependencies": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" } }, "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/helpers": ["@babel/helpers@7.27.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.3" } }, "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/traverse": ["@babel/traverse@7.27.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ=="],
|
||||||
|
|
||||||
|
"@astrojs/react/@vitejs/plugin-react/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
|
"widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||||
|
|
||||||
|
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@6.0.0", "", {}, "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg=="],
|
||||||
|
|
||||||
|
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.16.0", "", {}, "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ services:
|
|||||||
image: gitea/gitea:latest
|
image: gitea/gitea:latest
|
||||||
container_name: gitea
|
container_name: gitea
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
entrypoint: ["/tmp/gitea-dev-init.sh"]
|
||||||
environment:
|
environment:
|
||||||
- USER_UID=1000
|
- USER_UID=1000
|
||||||
- USER_GID=1000
|
- USER_GID=1000
|
||||||
- GITEA__database__DB_TYPE=sqlite3
|
- GITEA__database__DB_TYPE=sqlite3
|
||||||
- GITEA__database__PATH=/data/gitea.db
|
- GITEA__database__PATH=/data/gitea/gitea.db
|
||||||
- GITEA__server__DOMAIN=localhost
|
- GITEA__server__DOMAIN=localhost
|
||||||
- GITEA__server__ROOT_URL=http://localhost:3001/
|
- GITEA__server__ROOT_URL=http://localhost:3001/
|
||||||
- GITEA__server__SSH_DOMAIN=localhost
|
- GITEA__server__SSH_DOMAIN=localhost
|
||||||
@@ -19,20 +20,24 @@ services:
|
|||||||
- GITEA__server__START_SSH_SERVER=true
|
- GITEA__server__START_SSH_SERVER=true
|
||||||
- GITEA__security__INSTALL_LOCK=true
|
- GITEA__security__INSTALL_LOCK=true
|
||||||
- GITEA__service__DISABLE_REGISTRATION=false
|
- GITEA__service__DISABLE_REGISTRATION=false
|
||||||
|
- GITEA__log__MODE=console
|
||||||
|
- GITEA__log__LEVEL=Info
|
||||||
ports:
|
ports:
|
||||||
- "3001:3000"
|
- "3001:3000"
|
||||||
- "2222:22"
|
- "2222:22"
|
||||||
volumes:
|
volumes:
|
||||||
- gitea-data:/data
|
- gitea-data:/data
|
||||||
- gitea-config:/etc/gitea
|
- gitea-config:/etc/gitea
|
||||||
|
- ./scripts/gitea-app.ini:/tmp/app.ini:ro
|
||||||
|
- ./scripts/gitea-dev-init.sh:/tmp/gitea-dev-init.sh:ro
|
||||||
networks:
|
networks:
|
||||||
- gitea-network
|
- gitea-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/healthz"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 60s
|
||||||
|
|
||||||
# Development service connected to local Gitea
|
# Development service connected to local Gitea
|
||||||
gitea-mirror-dev:
|
gitea-mirror-dev:
|
||||||
@@ -63,6 +68,7 @@ services:
|
|||||||
- SKIP_FORKS=${SKIP_FORKS:-false}
|
- SKIP_FORKS=${SKIP_FORKS:-false}
|
||||||
- PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false}
|
- PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false}
|
||||||
- MIRROR_ISSUES=${MIRROR_ISSUES:-false}
|
- MIRROR_ISSUES=${MIRROR_ISSUES:-false}
|
||||||
|
- MIRROR_WIKI=${MIRROR_WIKI:-false}
|
||||||
- MIRROR_STARRED=${MIRROR_STARRED:-false}
|
- MIRROR_STARRED=${MIRROR_STARRED:-false}
|
||||||
- MIRROR_ORGANIZATIONS=${MIRROR_ORGANIZATIONS:-false}
|
- MIRROR_ORGANIZATIONS=${MIRROR_ORGANIZATIONS:-false}
|
||||||
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
gitea-mirror:
|
gitea-mirror:
|
||||||
image: ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-arunavo4/gitea-mirror}:${DOCKER_TAG:-latest}
|
image: ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-raylabshq/gitea-mirror}:${DOCKER_TAG:-latest}
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
- linux/amd64
|
- linux/amd64
|
||||||
- linux/arm64
|
- linux/arm64
|
||||||
cache_from:
|
cache_from:
|
||||||
- ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-arunavo4/gitea-mirror}:${DOCKER_TAG:-latest}
|
- ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-raylabshq/gitea-mirror}:${DOCKER_TAG:-latest}
|
||||||
container_name: gitea-mirror
|
container_name: gitea-mirror
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -30,6 +30,7 @@ services:
|
|||||||
- SKIP_FORKS=${SKIP_FORKS:-false}
|
- SKIP_FORKS=${SKIP_FORKS:-false}
|
||||||
- PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false}
|
- PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false}
|
||||||
- MIRROR_ISSUES=${MIRROR_ISSUES:-false}
|
- MIRROR_ISSUES=${MIRROR_ISSUES:-false}
|
||||||
|
- MIRROR_WIKI=${MIRROR_WIKI:-false}
|
||||||
- MIRROR_STARRED=${MIRROR_STARRED:-false}
|
- MIRROR_STARRED=${MIRROR_STARRED:-false}
|
||||||
- MIRROR_ORGANIZATIONS=${MIRROR_ORGANIZATIONS:-false}
|
- MIRROR_ORGANIZATIONS=${MIRROR_ORGANIZATIONS:-false}
|
||||||
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
||||||
|
|||||||
@@ -232,6 +232,28 @@ else
|
|||||||
echo "❌ Startup recovery failed with exit code $RECOVERY_EXIT_CODE"
|
echo "❌ Startup recovery failed with exit code $RECOVERY_EXIT_CODE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Run repository status repair to fix any inconsistent mirroring states
|
||||||
|
echo "Running repository status repair..."
|
||||||
|
if [ -f "dist/scripts/repair-mirrored-repos.js" ]; then
|
||||||
|
echo "Running repository repair using compiled script..."
|
||||||
|
bun dist/scripts/repair-mirrored-repos.js --startup
|
||||||
|
REPAIR_EXIT_CODE=$?
|
||||||
|
elif [ -f "scripts/repair-mirrored-repos.ts" ]; then
|
||||||
|
echo "Running repository repair using TypeScript script..."
|
||||||
|
bun scripts/repair-mirrored-repos.ts --startup
|
||||||
|
REPAIR_EXIT_CODE=$?
|
||||||
|
else
|
||||||
|
echo "Warning: Repository repair script not found. Skipping repair."
|
||||||
|
REPAIR_EXIT_CODE=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Log repair result
|
||||||
|
if [ $REPAIR_EXIT_CODE -eq 0 ]; then
|
||||||
|
echo "✅ Repository status repair completed successfully"
|
||||||
|
else
|
||||||
|
echo "⚠️ Repository status repair completed with warnings (exit code $REPAIR_EXIT_CODE)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Function to handle shutdown signals
|
# Function to handle shutdown signals
|
||||||
shutdown_handler() {
|
shutdown_handler() {
|
||||||
echo "🛑 Received shutdown signal, forwarding to application..."
|
echo "🛑 Received shutdown signal, forwarding to application..."
|
||||||
|
|||||||
72
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.9.3",
|
"version": "2.20.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
"check-db": "bun scripts/manage-db.ts check",
|
"check-db": "bun scripts/manage-db.ts check",
|
||||||
"fix-db": "bun scripts/manage-db.ts fix",
|
"fix-db": "bun scripts/manage-db.ts fix",
|
||||||
"reset-users": "bun scripts/manage-db.ts reset-users",
|
"reset-users": "bun scripts/manage-db.ts reset-users",
|
||||||
|
|
||||||
"startup-recovery": "bun scripts/startup-recovery.ts",
|
"startup-recovery": "bun scripts/startup-recovery.ts",
|
||||||
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
||||||
"test-recovery": "bun scripts/test-recovery.ts",
|
"test-recovery": "bun scripts/test-recovery.ts",
|
||||||
@@ -32,47 +31,54 @@
|
|||||||
"astro": "bunx --bun astro"
|
"astro": "bunx --bun astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.2.6",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/node": "^9.2.1",
|
"@astrojs/mdx": "^4.3.0",
|
||||||
"@astrojs/react": "^4.2.7",
|
"@astrojs/node": "^9.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@astrojs/react": "^4.3.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.9",
|
"@octokit/rest": "^22.0.0",
|
||||||
"@radix-ui/react-checkbox": "^1.3.1",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-dialog": "^1.1.13",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-popover": "^1.1.13",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-radio-group": "^1.3.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.11",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
"@tanstack/react-virtual": "^3.13.8",
|
"@tanstack/react-virtual": "^3.13.10",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.6",
|
||||||
"astro": "^5.7.13",
|
"astro": "^5.9.3",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.44.2",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.515.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.10",
|
||||||
"tw-animate-css": "^1.3.0",
|
"tw-animate-css": "^1.3.4",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.25.7"
|
"zod": "^3.25.64"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
@@ -80,10 +86,10 @@
|
|||||||
"@types/bcryptjs": "^3.0.0",
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.5.2",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"tsx": "^4.19.4",
|
"tsx": "^4.20.3",
|
||||||
"vitest": "^3.1.4"
|
"vitest": "^3.2.3"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.2.9"
|
"packageManager": "bun@1.2.16"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 21 KiB |
16
public/logo-dark.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
16
public/logo-light.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
16
public/logo.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
56
scripts/README-dev.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Development Environment Setup
|
||||||
|
|
||||||
|
This directory contains scripts to help set up a development environment with a pre-configured Gitea instance.
|
||||||
|
|
||||||
|
## Default Credentials
|
||||||
|
|
||||||
|
For development convenience, the Gitea instance is pre-configured with:
|
||||||
|
|
||||||
|
- **Admin Username**: `admin`
|
||||||
|
- **Admin Password**: `admin123`
|
||||||
|
- **Gitea URL**: http://localhost:3001
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `gitea-app.ini` - Pre-configured Gitea settings for development
|
||||||
|
- `gitea-dev-init.sh` - Initialization script that copies the config on first run
|
||||||
|
- `gitea-init.sql` - SQL script to create default admin user (not currently used)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Start the development environment:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
docker volume rm gitea-mirror_gitea-data gitea-mirror_gitea-config
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Wait for Gitea to start (check logs):
|
||||||
|
```bash
|
||||||
|
docker logs -f gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Access Gitea at http://localhost:3001 and login with:
|
||||||
|
- Username: `admin`
|
||||||
|
- Password: `admin123`
|
||||||
|
|
||||||
|
4. Generate an API token:
|
||||||
|
- Go to Settings → Applications
|
||||||
|
- Generate New Token
|
||||||
|
- Give it a name like "gitea-mirror"
|
||||||
|
- Select all permissions (for development)
|
||||||
|
- Copy the token
|
||||||
|
|
||||||
|
5. Configure gitea-mirror with the token in your `.env` file or through the web UI.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If Gitea doesn't start properly:
|
||||||
|
|
||||||
|
1. Check logs: `docker logs gitea`
|
||||||
|
2. Ensure volumes are clean: `docker volume rm gitea-mirror_gitea-data gitea-mirror_gitea-config`
|
||||||
|
3. Restart: `docker compose -f docker-compose.dev.yml up -d`
|
||||||
|
|
||||||
|
## Security Note
|
||||||
|
|
||||||
|
⚠️ **These credentials are for development only!** Never use these settings in production.
|
||||||
@@ -1,42 +1,47 @@
|
|||||||
# LXC Container Deployment Guide
|
# LXC Container Deployment Guide
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
Run **Gitea Mirror** in an isolated LXC container, either:
|
Run **Gitea Mirror** in an isolated LXC container:
|
||||||
|
|
||||||
1. **Online, on a Proxmox VE host** – script pulls everything from GitHub
|
1. **Proxmox VE (Recommended)** – Using the community-maintained script
|
||||||
2. **Offline / LAN-only, on a developer laptop** – script pushes your local checkout + Bun ZIP
|
2. **Local Development** – Using the local LXC script for testing
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Proxmox VE (online, recommended for prod)
|
## 1. Proxmox VE Installation (Recommended)
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
* Proxmox VE node with the default `vmbr0` bridge
|
* Proxmox VE host with internet access
|
||||||
* Root shell on the node
|
* Root shell access on the Proxmox node
|
||||||
* Ubuntu 22.04 LXC template present (`pveam update && pveam download ...`)
|
|
||||||
|
|
||||||
### One-command install
|
### One-command install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Community-maintained script for Proxmox VE by Tobias ([CrazyWolf13](https://github.com/CrazyWolf13))
|
# Community-maintained script from the Proxmox VE Community Scripts project
|
||||||
# at [community-scripts/ProxmoxVED](https://github.com/community-scripts/ProxmoxVED)
|
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"
|
||||||
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/install/gitea-mirror-install.sh)"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
What it does:
|
### What the script does:
|
||||||
|
|
||||||
* Uses the community-maintained script from ProxmoxVED
|
* Creates a privileged Alpine Linux LXC container
|
||||||
* Installs dependencies and Bun runtime
|
* Installs Bun runtime environment
|
||||||
* Clones & builds `arunavo4/gitea-mirror`
|
* Clones the Gitea Mirror repository
|
||||||
* Creates a systemd service and starts it
|
* Builds the application
|
||||||
* Sets up a random `JWT_SECRET` for security
|
* Configures a systemd service for automatic startup
|
||||||
|
* Sets up the application to run on port 4321
|
||||||
|
* Generates a secure `JWT_SECRET` automatically
|
||||||
|
|
||||||
Browse to:
|
### Accessing Gitea Mirror:
|
||||||
|
|
||||||
```
|
```
|
||||||
http://<container-ip>:4321
|
http://<container-ip>:4321
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Additional Information:
|
||||||
|
* **Script Source**: [Community Scripts for Proxmox VE](https://github.com/community-scripts/ProxmoxVE)
|
||||||
|
* **Documentation**: [Gitea Mirror Script Documentation](https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror)
|
||||||
|
* **Support**: [Community Scripts Discord](https://discord.gg/fiXVvSHnBU)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Local testing (LXD on a workstation, works offline)
|
## 2. Local testing (LXD on a workstation, works offline)
|
||||||
@@ -51,7 +56,7 @@ http://<container-ip>:4321
|
|||||||
### Offline installer script
|
### Offline installer script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/arunavo4/gitea-mirror.git # if not already
|
git clone https://github.com/RayLabsHQ/gitea-mirror.git # if not already
|
||||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
|
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
|
||||||
chmod +x gitea-mirror-lxc-local.sh
|
chmod +x gitea-mirror-lxc-local.sh
|
||||||
|
|
||||||
|
|||||||
129
scripts/cleanup-duplicate-repos.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to find and clean up duplicate repositories in the database
|
||||||
|
* Keeps the most recent entry and removes older duplicates
|
||||||
|
*
|
||||||
|
* Usage: bun scripts/cleanup-duplicate-repos.ts [--dry-run] [--repo-name=<name>]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db, repositories, mirrorJobs } from "@/lib/db";
|
||||||
|
import { eq, and, desc } from "drizzle-orm";
|
||||||
|
|
||||||
|
const isDryRun = process.argv.includes("--dry-run");
|
||||||
|
const specificRepo = process.argv.find(arg => arg.startsWith("--repo-name="))?.split("=")[1];
|
||||||
|
|
||||||
|
async function findDuplicateRepositories() {
|
||||||
|
console.log("🔍 Finding duplicate repositories");
|
||||||
|
console.log("=" .repeat(40));
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
console.log("🔍 DRY RUN MODE - No changes will be made");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specificRepo) {
|
||||||
|
console.log(`🎯 Targeting specific repository: ${specificRepo}`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find all repositories, grouped by name and fullName
|
||||||
|
let allRepos = await db.select().from(repositories);
|
||||||
|
|
||||||
|
if (specificRepo) {
|
||||||
|
allRepos = allRepos.filter(repo => repo.name === specificRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group repositories by name and fullName
|
||||||
|
const repoGroups = new Map<string, typeof allRepos>();
|
||||||
|
|
||||||
|
for (const repo of allRepos) {
|
||||||
|
const key = `${repo.name}|${repo.fullName}`;
|
||||||
|
if (!repoGroups.has(key)) {
|
||||||
|
repoGroups.set(key, []);
|
||||||
|
}
|
||||||
|
repoGroups.get(key)!.push(repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find groups with duplicates
|
||||||
|
const duplicateGroups = Array.from(repoGroups.entries())
|
||||||
|
.filter(([_, repos]) => repos.length > 1);
|
||||||
|
|
||||||
|
if (duplicateGroups.length === 0) {
|
||||||
|
console.log("✅ No duplicate repositories found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Found ${duplicateGroups.length} sets of duplicate repositories:`);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
let totalDuplicates = 0;
|
||||||
|
let totalRemoved = 0;
|
||||||
|
|
||||||
|
for (const [key, repos] of duplicateGroups) {
|
||||||
|
const [name, fullName] = key.split("|");
|
||||||
|
console.log(`🔄 Processing duplicates for: ${name} (${fullName})`);
|
||||||
|
console.log(` Found ${repos.length} entries:`);
|
||||||
|
|
||||||
|
// Sort by updatedAt descending to keep the most recent
|
||||||
|
repos.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||||
|
|
||||||
|
const keepRepo = repos[0];
|
||||||
|
const removeRepos = repos.slice(1);
|
||||||
|
|
||||||
|
console.log(` ✅ Keeping: ID ${keepRepo.id} (Status: ${keepRepo.status}, Updated: ${new Date(keepRepo.updatedAt).toISOString()})`);
|
||||||
|
|
||||||
|
for (const repo of removeRepos) {
|
||||||
|
console.log(` ❌ Removing: ID ${repo.id} (Status: ${repo.status}, Updated: ${new Date(repo.updatedAt).toISOString()})`);
|
||||||
|
|
||||||
|
if (!isDryRun) {
|
||||||
|
try {
|
||||||
|
// First, delete related mirror jobs
|
||||||
|
await db
|
||||||
|
.delete(mirrorJobs)
|
||||||
|
.where(eq(mirrorJobs.repositoryId, repo.id!));
|
||||||
|
|
||||||
|
// Then delete the repository
|
||||||
|
await db
|
||||||
|
.delete(repositories)
|
||||||
|
.where(eq(repositories.id, repo.id!));
|
||||||
|
|
||||||
|
console.log(` 🗑️ Deleted repository and related mirror jobs`);
|
||||||
|
totalRemoved++;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ❌ Error deleting repository: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` 🗑️ Would delete repository and related mirror jobs`);
|
||||||
|
totalRemoved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDuplicates += removeRepos.length;
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📊 Cleanup Summary:");
|
||||||
|
console.log(` Duplicate sets found: ${duplicateGroups.length}`);
|
||||||
|
console.log(` Total duplicates: ${totalDuplicates}`);
|
||||||
|
console.log(` ${isDryRun ? 'Would remove' : 'Removed'}: ${totalRemoved}`);
|
||||||
|
|
||||||
|
if (isDryRun && totalRemoved > 0) {
|
||||||
|
console.log("");
|
||||||
|
console.log("💡 To apply these changes, run the script without --dry-run");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error during cleanup process:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the cleanup
|
||||||
|
findDuplicateRepositories().then(() => {
|
||||||
|
console.log("Cleanup process complete.");
|
||||||
|
process.exit(0);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
68
scripts/gitea-app.ini
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
APP_NAME = Gitea: Git with a cup of tea
|
||||||
|
RUN_MODE = prod
|
||||||
|
|
||||||
|
[database]
|
||||||
|
DB_TYPE = sqlite3
|
||||||
|
PATH = /data/gitea/gitea.db
|
||||||
|
|
||||||
|
[repository]
|
||||||
|
ROOT = /data/git/repositories
|
||||||
|
|
||||||
|
[server]
|
||||||
|
SSH_DOMAIN = localhost
|
||||||
|
DOMAIN = localhost
|
||||||
|
HTTP_PORT = 3000
|
||||||
|
ROOT_URL = http://localhost:3001/
|
||||||
|
DISABLE_SSH = false
|
||||||
|
SSH_PORT = 22
|
||||||
|
LFS_START_SERVER = true
|
||||||
|
LFS_JWT_SECRET = _oaWNP5sCH5cSECa-K_HvCXeXhg-zN5H0cU5vVQAZr4
|
||||||
|
OFFLINE_MODE = false
|
||||||
|
|
||||||
|
[security]
|
||||||
|
INSTALL_LOCK = true
|
||||||
|
SECRET_KEY = vLu5OuX0EweZjDNxKPQ5V9DXXXX8cJiKpJyQylKkMVTrNdFAzlUlNdYLYfiCybu
|
||||||
|
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MjgzMTk1MDB9.Lz0cJB_DCLmJFh8FqDX0z9IUcxfY9jPftHEGvz_WeHo
|
||||||
|
PASSWORD_HASH_ALGO = pbkdf2
|
||||||
|
|
||||||
|
[service]
|
||||||
|
DISABLE_REGISTRATION = false
|
||||||
|
REQUIRE_SIGNIN_VIEW = false
|
||||||
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
|
ENABLE_NOTIFY_MAIL = false
|
||||||
|
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||||
|
ENABLE_CAPTCHA = false
|
||||||
|
DEFAULT_KEEP_EMAIL_PRIVATE = false
|
||||||
|
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
||||||
|
DEFAULT_ENABLE_TIMETRACKING = true
|
||||||
|
NO_REPLY_ADDRESS = noreply.localhost
|
||||||
|
|
||||||
|
[oauth2]
|
||||||
|
JWT_SECRET = gQXt_D8B-VJGCvFfJ9xEj5yp8mOd6fAza8TKc9rJJYw
|
||||||
|
|
||||||
|
[lfs]
|
||||||
|
PATH = /data/git/lfs
|
||||||
|
|
||||||
|
[mailer]
|
||||||
|
ENABLED = false
|
||||||
|
|
||||||
|
[openid]
|
||||||
|
ENABLE_OPENID_SIGNIN = true
|
||||||
|
ENABLE_OPENID_SIGNUP = true
|
||||||
|
|
||||||
|
[session]
|
||||||
|
PROVIDER = file
|
||||||
|
|
||||||
|
[log]
|
||||||
|
MODE = console
|
||||||
|
LEVEL = Info
|
||||||
|
ROOT_PATH = /data/gitea/log
|
||||||
|
|
||||||
|
[repository.pull-request]
|
||||||
|
DEFAULT_MERGE_STYLE = merge
|
||||||
|
|
||||||
|
[repository.signing]
|
||||||
|
DEFAULT_TRUST_MODEL = committer
|
||||||
|
|
||||||
|
[actions]
|
||||||
|
ENABLED = false
|
||||||
14
scripts/gitea-create-admin.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Create admin user for Gitea development instance
|
||||||
|
|
||||||
|
echo "Creating admin user for Gitea..."
|
||||||
|
docker exec -u git gitea gitea admin user create \
|
||||||
|
--username admin \
|
||||||
|
--password admin123 \
|
||||||
|
--email admin@localhost \
|
||||||
|
--admin \
|
||||||
|
--must-change-password=false
|
||||||
|
|
||||||
|
echo "Admin user created!"
|
||||||
|
echo "Username: admin"
|
||||||
|
echo "Password: admin123"
|
||||||
32
scripts/gitea-dev-init.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Initialize Gitea for development with pre-configured settings
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
mkdir -p /data/gitea/conf
|
||||||
|
|
||||||
|
# Copy pre-configured app.ini if it doesn't exist
|
||||||
|
if [ ! -f /data/gitea/conf/app.ini ]; then
|
||||||
|
echo "Initializing Gitea with development configuration..."
|
||||||
|
cp /tmp/app.ini /data/gitea/conf/app.ini
|
||||||
|
chown 1000:1000 /data/gitea/conf/app.ini
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Gitea in background
|
||||||
|
/usr/bin/entrypoint "$@" &
|
||||||
|
GITEA_PID=$!
|
||||||
|
|
||||||
|
# Wait for Gitea to be ready
|
||||||
|
echo "Waiting for Gitea to start..."
|
||||||
|
until wget --no-verbose --tries=1 --spider http://localhost:3000/ 2>/dev/null; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create admin user if it doesn't exist
|
||||||
|
if [ ! -f /data/.admin_created ]; then
|
||||||
|
echo "Creating default admin user..."
|
||||||
|
su git -c "gitea admin user create --username admin --password admin123 --email admin@localhost --admin --must-change-password=false" && \
|
||||||
|
touch /data/.admin_created
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep Gitea running in foreground
|
||||||
|
wait $GITEA_PID
|
||||||
178
scripts/investigate-repo.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to investigate a specific repository's mirroring status
|
||||||
|
* Usage: bun scripts/investigate-repo.ts [repository-name]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db, repositories, mirrorJobs, configs } from "@/lib/db";
|
||||||
|
import { eq, desc, and } from "drizzle-orm";
|
||||||
|
|
||||||
|
const repoName = process.argv[2] || "EruditionPaper";
|
||||||
|
|
||||||
|
async function investigateRepository() {
|
||||||
|
console.log(`🔍 Investigating repository: ${repoName}`);
|
||||||
|
console.log("=" .repeat(50));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the repository in the database
|
||||||
|
const repos = await db
|
||||||
|
.select()
|
||||||
|
.from(repositories)
|
||||||
|
.where(eq(repositories.name, repoName));
|
||||||
|
|
||||||
|
if (repos.length === 0) {
|
||||||
|
console.log(`❌ Repository "${repoName}" not found in database`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = repos[0];
|
||||||
|
console.log(`✅ Found repository: ${repo.name}`);
|
||||||
|
console.log(` ID: ${repo.id}`);
|
||||||
|
console.log(` Full Name: ${repo.fullName}`);
|
||||||
|
console.log(` Owner: ${repo.owner}`);
|
||||||
|
console.log(` Organization: ${repo.organization || "None"}`);
|
||||||
|
console.log(` Status: ${repo.status}`);
|
||||||
|
console.log(` Is Private: ${repo.isPrivate}`);
|
||||||
|
console.log(` Is Forked: ${repo.isForked}`);
|
||||||
|
console.log(` Mirrored Location: ${repo.mirroredLocation || "Not set"}`);
|
||||||
|
console.log(` Last Mirrored: ${repo.lastMirrored ? new Date(repo.lastMirrored).toISOString() : "Never"}`);
|
||||||
|
console.log(` Error Message: ${repo.errorMessage || "None"}`);
|
||||||
|
console.log(` Created At: ${new Date(repo.createdAt).toISOString()}`);
|
||||||
|
console.log(` Updated At: ${new Date(repo.updatedAt).toISOString()}`);
|
||||||
|
|
||||||
|
console.log("\n📋 Recent Mirror Jobs:");
|
||||||
|
console.log("-".repeat(30));
|
||||||
|
|
||||||
|
// Find recent mirror jobs for this repository
|
||||||
|
const jobs = await db
|
||||||
|
.select()
|
||||||
|
.from(mirrorJobs)
|
||||||
|
.where(eq(mirrorJobs.repositoryId, repo.id))
|
||||||
|
.orderBy(desc(mirrorJobs.timestamp))
|
||||||
|
.limit(10);
|
||||||
|
|
||||||
|
if (jobs.length === 0) {
|
||||||
|
console.log(" No mirror jobs found for this repository");
|
||||||
|
} else {
|
||||||
|
jobs.forEach((job, index) => {
|
||||||
|
console.log(` ${index + 1}. ${new Date(job.timestamp).toISOString()}`);
|
||||||
|
console.log(` Status: ${job.status}`);
|
||||||
|
console.log(` Message: ${job.message}`);
|
||||||
|
if (job.details) {
|
||||||
|
console.log(` Details: ${job.details}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user configuration
|
||||||
|
console.log("⚙️ User Configuration:");
|
||||||
|
console.log("-".repeat(20));
|
||||||
|
|
||||||
|
const config = await db
|
||||||
|
.select()
|
||||||
|
.from(configs)
|
||||||
|
.where(eq(configs.id, repo.configId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (config.length > 0) {
|
||||||
|
const userConfig = config[0];
|
||||||
|
console.log(` User ID: ${userConfig.userId}`);
|
||||||
|
console.log(` GitHub Username: ${userConfig.githubConfig?.username || "Not set"}`);
|
||||||
|
console.log(` Gitea URL: ${userConfig.giteaConfig?.url || "Not set"}`);
|
||||||
|
console.log(` Gitea Username: ${userConfig.giteaConfig?.username || "Not set"}`);
|
||||||
|
console.log(` Preserve Org Structure: ${userConfig.githubConfig?.preserveOrgStructure || false}`);
|
||||||
|
console.log(` Mirror Issues: ${userConfig.githubConfig?.mirrorIssues || false}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for any active jobs
|
||||||
|
console.log("\n🔄 Active Jobs:");
|
||||||
|
console.log("-".repeat(15));
|
||||||
|
|
||||||
|
const activeJobs = await db
|
||||||
|
.select()
|
||||||
|
.from(mirrorJobs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(mirrorJobs.repositoryId, repo.id),
|
||||||
|
eq(mirrorJobs.inProgress, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (activeJobs.length === 0) {
|
||||||
|
console.log(" No active jobs found");
|
||||||
|
} else {
|
||||||
|
activeJobs.forEach((job, index) => {
|
||||||
|
console.log(` ${index + 1}. Job ID: ${job.id}`);
|
||||||
|
console.log(` Type: ${job.jobType || "mirror"}`);
|
||||||
|
console.log(` Batch ID: ${job.batchId || "None"}`);
|
||||||
|
console.log(` Started: ${job.startedAt ? new Date(job.startedAt).toISOString() : "Unknown"}`);
|
||||||
|
console.log(` Last Checkpoint: ${job.lastCheckpoint ? new Date(job.lastCheckpoint).toISOString() : "None"}`);
|
||||||
|
console.log(` Progress: ${job.completedItems || 0}/${job.totalItems || 0}`);
|
||||||
|
console.log("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if repository exists in Gitea
|
||||||
|
if (config.length > 0) {
|
||||||
|
const userConfig = config[0];
|
||||||
|
console.log("\n🔗 Gitea Repository Check:");
|
||||||
|
console.log("-".repeat(25));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const giteaUrl = userConfig.giteaConfig?.url;
|
||||||
|
const giteaToken = userConfig.giteaConfig?.token;
|
||||||
|
const giteaUsername = userConfig.giteaConfig?.username;
|
||||||
|
|
||||||
|
if (giteaUrl && giteaToken && giteaUsername) {
|
||||||
|
const checkUrl = `${giteaUrl}/api/v1/repos/${giteaUsername}/${repo.name}`;
|
||||||
|
console.log(` Checking: ${checkUrl}`);
|
||||||
|
|
||||||
|
const response = await fetch(checkUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${giteaToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` Response Status: ${response.status} ${response.statusText}`);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const repoData = await response.json();
|
||||||
|
console.log(` ✅ Repository exists in Gitea`);
|
||||||
|
console.log(` Name: ${repoData.name}`);
|
||||||
|
console.log(` Full Name: ${repoData.full_name}`);
|
||||||
|
console.log(` Private: ${repoData.private}`);
|
||||||
|
console.log(` Mirror: ${repoData.mirror}`);
|
||||||
|
console.log(` Clone URL: ${repoData.clone_url}`);
|
||||||
|
console.log(` Created: ${new Date(repoData.created_at).toISOString()}`);
|
||||||
|
console.log(` Updated: ${new Date(repoData.updated_at).toISOString()}`);
|
||||||
|
if (repoData.mirror_updated) {
|
||||||
|
console.log(` Mirror Updated: ${new Date(repoData.mirror_updated).toISOString()}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` ❌ Repository not found in Gitea`);
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.log(` Error: ${errorText}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(" ⚠️ Missing Gitea configuration");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ❌ Error checking Gitea: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error investigating repository:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the investigation
|
||||||
|
investigateRepository().then(() => {
|
||||||
|
console.log("Investigation complete.");
|
||||||
|
process.exit(0);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
277
scripts/repair-mirrored-repos.ts
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to repair repositories that exist in Gitea but have incorrect status in the database
|
||||||
|
* This fixes the issue where repositories show as "imported" but are actually mirrored in Gitea
|
||||||
|
*
|
||||||
|
* Usage: bun scripts/repair-mirrored-repos.ts [--dry-run] [--repo-name=<name>]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { db, repositories, configs } from "@/lib/db";
|
||||||
|
import { eq, and, or } from "drizzle-orm";
|
||||||
|
import { createMirrorJob } from "@/lib/helpers";
|
||||||
|
import { repoStatusEnum } from "@/types/Repository";
|
||||||
|
|
||||||
|
const isDryRun = process.argv.includes("--dry-run");
|
||||||
|
const specificRepo = process.argv.find(arg => arg.startsWith("--repo-name="))?.split("=")[1];
|
||||||
|
const isStartupMode = process.argv.includes("--startup");
|
||||||
|
|
||||||
|
async function checkRepoInGitea(config: any, owner: string, repoName: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${config.giteaConfig.token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking repo ${owner}/${repoName} in Gitea:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRepoDetailsFromGitea(config: any, owner: string, repoName: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${config.giteaConfig.token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting repo details for ${owner}/${repoName}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repairMirroredRepositories() {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log("🔧 Repairing mirrored repositories database status");
|
||||||
|
console.log("=" .repeat(60));
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
console.log("🔍 DRY RUN MODE - No changes will be made");
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specificRepo) {
|
||||||
|
console.log(`🎯 Targeting specific repository: ${specificRepo}`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find repositories that might need repair
|
||||||
|
let query = db
|
||||||
|
.select()
|
||||||
|
.from(repositories)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
eq(repositories.status, "imported"),
|
||||||
|
eq(repositories.status, "failed")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (specificRepo) {
|
||||||
|
query = query.where(eq(repositories.name, specificRepo));
|
||||||
|
}
|
||||||
|
|
||||||
|
const repos = await query;
|
||||||
|
|
||||||
|
if (repos.length === 0) {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log("✅ No repositories found that need repair");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(`📋 Found ${repos.length} repositories to check:`);
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
|
||||||
|
let repairedCount = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (const repo of repos) {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(`🔍 Checking repository: ${repo.name}`);
|
||||||
|
console.log(` Current status: ${repo.status}`);
|
||||||
|
console.log(` Mirrored location: ${repo.mirroredLocation || "Not set"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get user configuration
|
||||||
|
const config = await db
|
||||||
|
.select()
|
||||||
|
.from(configs)
|
||||||
|
.where(eq(configs.id, repo.configId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (config.length === 0) {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(` ❌ No configuration found for repository`);
|
||||||
|
}
|
||||||
|
errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userConfig = config[0];
|
||||||
|
const giteaUsername = userConfig.giteaConfig?.username;
|
||||||
|
|
||||||
|
if (!giteaUsername) {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(` ❌ No Gitea username in configuration`);
|
||||||
|
}
|
||||||
|
errorCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if repository exists in Gitea (try both user and organization)
|
||||||
|
let existsInGitea = false;
|
||||||
|
let actualOwner = giteaUsername;
|
||||||
|
let giteaRepoDetails = null;
|
||||||
|
|
||||||
|
// First check user location
|
||||||
|
existsInGitea = await checkRepoInGitea(userConfig, giteaUsername, repo.name);
|
||||||
|
if (existsInGitea) {
|
||||||
|
giteaRepoDetails = await getRepoDetailsFromGitea(userConfig, giteaUsername, repo.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found in user location and repo has organization, check organization
|
||||||
|
if (!existsInGitea && repo.organization) {
|
||||||
|
existsInGitea = await checkRepoInGitea(userConfig, repo.organization, repo.name);
|
||||||
|
if (existsInGitea) {
|
||||||
|
actualOwner = repo.organization;
|
||||||
|
giteaRepoDetails = await getRepoDetailsFromGitea(userConfig, repo.organization, repo.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsInGitea) {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(` ⏭️ Repository not found in Gitea - skipping`);
|
||||||
|
}
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(` ✅ Repository found in Gitea at: ${actualOwner}/${repo.name}`);
|
||||||
|
|
||||||
|
if (giteaRepoDetails) {
|
||||||
|
console.log(` 📊 Gitea details:`);
|
||||||
|
console.log(` Mirror: ${giteaRepoDetails.mirror}`);
|
||||||
|
console.log(` Created: ${new Date(giteaRepoDetails.created_at).toISOString()}`);
|
||||||
|
console.log(` Updated: ${new Date(giteaRepoDetails.updated_at).toISOString()}`);
|
||||||
|
if (giteaRepoDetails.mirror_updated) {
|
||||||
|
console.log(` Mirror Updated: ${new Date(giteaRepoDetails.mirror_updated).toISOString()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (repairedCount === 0) {
|
||||||
|
// In startup mode, only log the first repair to indicate activity
|
||||||
|
console.log(`Repairing repository status inconsistencies...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDryRun) {
|
||||||
|
// Update repository status in database
|
||||||
|
const mirrorUpdated = giteaRepoDetails?.mirror_updated
|
||||||
|
? new Date(giteaRepoDetails.mirror_updated)
|
||||||
|
: new Date();
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(repositories)
|
||||||
|
.set({
|
||||||
|
status: repoStatusEnum.parse("mirrored"),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
lastMirrored: mirrorUpdated,
|
||||||
|
errorMessage: null,
|
||||||
|
mirroredLocation: `${actualOwner}/${repo.name}`,
|
||||||
|
})
|
||||||
|
.where(eq(repositories.id, repo.id!));
|
||||||
|
|
||||||
|
// Create a mirror job log entry
|
||||||
|
await createMirrorJob({
|
||||||
|
userId: userConfig.userId || "",
|
||||||
|
repositoryId: repo.id,
|
||||||
|
repositoryName: repo.name,
|
||||||
|
message: `Repository status repaired - found existing mirror in Gitea`,
|
||||||
|
details: `Repository ${repo.name} was found to already exist in Gitea at ${actualOwner}/${repo.name} and database status was updated from ${repo.status} to mirrored.`,
|
||||||
|
status: "mirrored",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(` 🔧 Repaired: Updated status to 'mirrored'`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(` 🔧 Would repair: Update status from '${repo.status}' to 'mirrored'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repairedCount++;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log(` ❌ Error processing repository: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isStartupMode) {
|
||||||
|
console.log("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStartupMode) {
|
||||||
|
// In startup mode, only log if there were repairs or errors
|
||||||
|
if (repairedCount > 0) {
|
||||||
|
console.log(`Repaired ${repairedCount} repository status inconsistencies`);
|
||||||
|
}
|
||||||
|
if (errorCount > 0) {
|
||||||
|
console.log(`Warning: ${errorCount} repositories had errors during repair`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("📊 Repair Summary:");
|
||||||
|
console.log(` Repaired: ${repairedCount}`);
|
||||||
|
console.log(` Skipped: ${skippedCount}`);
|
||||||
|
console.log(` Errors: ${errorCount}`);
|
||||||
|
|
||||||
|
if (isDryRun && repairedCount > 0) {
|
||||||
|
console.log("");
|
||||||
|
console.log("💡 To apply these changes, run the script without --dry-run");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error during repair process:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the repair
|
||||||
|
repairMirroredRepositories().then(() => {
|
||||||
|
console.log("Repair process complete.");
|
||||||
|
process.exit(0);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
81
src/components/NotFound.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";
|
||||||
|
|
||||||
|
export function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="h-dvh bg-muted/30 flex items-center justify-center p-4">
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader className="text-center pb-4">
|
||||||
|
<div className="mx-auto mb-4 h-16 w-16 rounded-full bg-muted flex items-center justify-center">
|
||||||
|
<FileQuestion className="h-8 w-8 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold">404</h1>
|
||||||
|
<h2 className="text-xl font-semibold mt-2">Page Not Found</h2>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
The page you're looking for doesn't exist or has been moved.
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Button asChild className="w-full">
|
||||||
|
<a href="/">
|
||||||
|
<Home className="mr-2 h-4 w-4" />
|
||||||
|
Go to Dashboard
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="w-full" onClick={() => window.history.back()}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-card px-2 text-muted-foreground">or visit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<a
|
||||||
|
href="/repositories"
|
||||||
|
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<span className="text-xs">Repositories</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/config"
|
||||||
|
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<span className="text-xs">Config</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/docs"
|
||||||
|
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<BookOpen className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<span className="text-xs">Docs</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Code */}
|
||||||
|
<div className="text-center pt-2">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Error Code: <code className="font-mono">404_NOT_FOUND</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { SiGitea } from 'react-icons/si';
|
|
||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
import { showErrorToast } from '@/lib/utils';
|
import { showErrorToast } from '@/lib/utils';
|
||||||
import { FlipHorizontal } from 'lucide-react';
|
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -60,7 +60,16 @@ export function LoginForm() {
|
|||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<SiGitea className="h-10 w-10" />
|
<img
|
||||||
|
src="/logo-light.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-10 w-10 dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/logo-dark.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-10 w-10 hidden dark:block"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl">Gitea Mirror</CardTitle>
|
<CardTitle className="text-2xl">Gitea Mirror</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { GitMerge } from 'lucide-react';
|
|
||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
import { showErrorToast } from '@/lib/utils';
|
import { showErrorToast } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -66,7 +65,16 @@ export function SignupForm() {
|
|||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
<GitMerge className="h-10 w-10" />
|
<img
|
||||||
|
src="/logo-light.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-10 w-10 dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/logo-dark.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-10 w-10 hidden dark:block"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardTitle className="text-2xl">Create Admin Account</CardTitle>
|
<CardTitle className="text-2xl">Create Admin Account</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
|
|||||||
90
src/components/config/AdvancedOptionsForm.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Checkbox } from "../ui/checkbox";
|
||||||
|
import type { AdvancedOptions } from "@/types/config";
|
||||||
|
import { RefreshCw } from "lucide-react";
|
||||||
|
|
||||||
|
interface AdvancedOptionsFormProps {
|
||||||
|
config: AdvancedOptions;
|
||||||
|
setConfig: React.Dispatch<React.SetStateAction<AdvancedOptions>>;
|
||||||
|
onAutoSave?: (config: AdvancedOptions) => Promise<void>;
|
||||||
|
isAutoSaving?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdvancedOptionsForm({
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
onAutoSave,
|
||||||
|
isAutoSaving = false,
|
||||||
|
}: AdvancedOptionsFormProps) {
|
||||||
|
const handleChange = (name: string, checked: boolean) => {
|
||||||
|
const newConfig = {
|
||||||
|
...config,
|
||||||
|
[name]: checked,
|
||||||
|
};
|
||||||
|
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
|
// Auto-save
|
||||||
|
if (onAutoSave) {
|
||||||
|
onAutoSave(newConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="self-start">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg font-semibold flex items-center justify-between">
|
||||||
|
Advanced Options
|
||||||
|
{isAutoSaving && (
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
|
||||||
|
<span className="text-xs">Auto-saving...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="skip-forks"
|
||||||
|
checked={config.skipForks}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("skipForks", Boolean(checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="skip-forks"
|
||||||
|
className="ml-2 text-sm select-none"
|
||||||
|
>
|
||||||
|
Skip forks
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground ml-6">
|
||||||
|
Don't mirror repositories that are forks of other repositories
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="skip-starred-issues"
|
||||||
|
checked={config.skipStarredIssues}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("skipStarredIssues", Boolean(checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="skip-starred-issues"
|
||||||
|
className="ml-2 text-sm select-none"
|
||||||
|
>
|
||||||
|
Don't fetch issues for starred repos
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground ml-6">
|
||||||
|
Skip mirroring issues and pull requests for starred repositories
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
332
src/components/config/AutomationSettings.tsx
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
Database,
|
||||||
|
RefreshCw,
|
||||||
|
Calendar,
|
||||||
|
Activity,
|
||||||
|
Zap,
|
||||||
|
Info
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import type { ScheduleConfig, DatabaseCleanupConfig } from "@/types/config";
|
||||||
|
import { formatDate } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface AutomationSettingsProps {
|
||||||
|
scheduleConfig: ScheduleConfig;
|
||||||
|
cleanupConfig: DatabaseCleanupConfig;
|
||||||
|
onScheduleChange: (config: ScheduleConfig) => void;
|
||||||
|
onCleanupChange: (config: DatabaseCleanupConfig) => void;
|
||||||
|
isAutoSavingSchedule?: boolean;
|
||||||
|
isAutoSavingCleanup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduleIntervals = [
|
||||||
|
{ label: "Every hour", value: 3600 },
|
||||||
|
{ label: "Every 2 hours", value: 7200 },
|
||||||
|
{ label: "Every 4 hours", value: 14400 },
|
||||||
|
{ label: "Every 8 hours", value: 28800 },
|
||||||
|
{ label: "Every 12 hours", value: 43200 },
|
||||||
|
{ label: "Daily", value: 86400 },
|
||||||
|
{ label: "Every 2 days", value: 172800 },
|
||||||
|
{ label: "Weekly", value: 604800 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const retentionPeriods = [
|
||||||
|
{ label: "1 day", value: 86400 },
|
||||||
|
{ label: "3 days", value: 259200 },
|
||||||
|
{ label: "1 week", value: 604800 },
|
||||||
|
{ label: "2 weeks", value: 1209600 },
|
||||||
|
{ label: "1 month", value: 2592000 },
|
||||||
|
{ label: "2 months", value: 5184000 },
|
||||||
|
{ label: "3 months", value: 7776000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getCleanupInterval(retentionSeconds: number): number {
|
||||||
|
const days = retentionSeconds / 86400;
|
||||||
|
if (days <= 1) return 21600; // 6 hours
|
||||||
|
if (days <= 3) return 43200; // 12 hours
|
||||||
|
if (days <= 7) return 86400; // 24 hours
|
||||||
|
if (days <= 30) return 172800; // 48 hours
|
||||||
|
return 604800; // 1 week
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCleanupFrequencyText(retentionSeconds: number): string {
|
||||||
|
const days = retentionSeconds / 86400;
|
||||||
|
if (days <= 1) return "every 6 hours";
|
||||||
|
if (days <= 3) return "every 12 hours";
|
||||||
|
if (days <= 7) return "daily";
|
||||||
|
if (days <= 30) return "every 2 days";
|
||||||
|
return "weekly";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AutomationSettings({
|
||||||
|
scheduleConfig,
|
||||||
|
cleanupConfig,
|
||||||
|
onScheduleChange,
|
||||||
|
onCleanupChange,
|
||||||
|
isAutoSavingSchedule,
|
||||||
|
isAutoSavingCleanup,
|
||||||
|
}: AutomationSettingsProps) {
|
||||||
|
// Update nextRun for cleanup when settings change
|
||||||
|
useEffect(() => {
|
||||||
|
if (cleanupConfig.enabled && !cleanupConfig.nextRun) {
|
||||||
|
const cleanupInterval = getCleanupInterval(cleanupConfig.retentionDays);
|
||||||
|
const nextRun = new Date(Date.now() + cleanupInterval * 1000);
|
||||||
|
onCleanupChange({ ...cleanupConfig, nextRun });
|
||||||
|
}
|
||||||
|
}, [cleanupConfig.enabled, cleanupConfig.retentionDays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg font-semibold flex items-center gap-2">
|
||||||
|
<Zap className="h-5 w-5" />
|
||||||
|
Automation & Maintenance
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Automatic Mirroring Section */}
|
||||||
|
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<RefreshCw className="h-4 w-4 text-primary" />
|
||||||
|
Automatic Mirroring
|
||||||
|
</h3>
|
||||||
|
{isAutoSavingSchedule && (
|
||||||
|
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="enable-auto-mirror"
|
||||||
|
checked={scheduleConfig.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onScheduleChange({ ...scheduleConfig, enabled: !!checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="enable-auto-mirror"
|
||||||
|
className="text-sm font-normal cursor-pointer"
|
||||||
|
>
|
||||||
|
Enable automatic repository syncing
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Periodically check GitHub for changes and mirror them to Gitea
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{scheduleConfig.enabled && (
|
||||||
|
<div className="ml-6 space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="mirror-interval" className="text-sm">
|
||||||
|
Sync frequency
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={scheduleConfig.interval.toString()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onScheduleChange({
|
||||||
|
...scheduleConfig,
|
||||||
|
interval: parseInt(value, 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="mirror-interval" className="mt-1.5">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{scheduleIntervals.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value.toString()}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2 p-3 bg-muted/30 dark:bg-muted/10 rounded-md border border-border/50">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Clock className="h-3.5 w-3.5" />
|
||||||
|
Last sync
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{scheduleConfig.lastRun
|
||||||
|
? formatDate(scheduleConfig.lastRun)
|
||||||
|
: "Never"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{scheduleConfig.enabled && scheduleConfig.nextRun && (
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
|
Next sync
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{formatDate(scheduleConfig.nextRun)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Database Cleanup Section */}
|
||||||
|
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4 text-primary" />
|
||||||
|
Database Maintenance
|
||||||
|
</h3>
|
||||||
|
{isAutoSavingCleanup && (
|
||||||
|
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="enable-auto-cleanup"
|
||||||
|
checked={cleanupConfig.enabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onCleanupChange({ ...cleanupConfig, enabled: !!checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="enable-auto-cleanup"
|
||||||
|
className="text-sm font-normal cursor-pointer"
|
||||||
|
>
|
||||||
|
Enable automatic database cleanup
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Remove old activity logs and events to optimize storage
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cleanupConfig.enabled && (
|
||||||
|
<div className="ml-6 space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="retention-period" className="text-sm flex items-center gap-2">
|
||||||
|
Data retention period
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3 w-3 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-xs">
|
||||||
|
<p className="text-xs">
|
||||||
|
Activity logs and events older than this will be removed.
|
||||||
|
Cleanup frequency is automatically optimized based on your retention period.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={cleanupConfig.retentionDays.toString()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onCleanupChange({
|
||||||
|
...cleanupConfig,
|
||||||
|
retentionDays: parseInt(value, 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="retention-period" className="mt-1.5">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{retentionPeriods.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value.toString()}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{cleanupConfig.enabled && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Cleanup runs {getCleanupFrequencyText(cleanupConfig.retentionDays)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2 p-3 bg-muted/30 dark:bg-muted/10 rounded-md border border-border/50">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Clock className="h-3.5 w-3.5" />
|
||||||
|
Last cleanup
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{cleanupConfig.lastRun
|
||||||
|
? formatDate(cleanupConfig.lastRun)
|
||||||
|
: "Never"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{cleanupConfig.enabled && cleanupConfig.nextRun && (
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
|
Next cleanup
|
||||||
|
</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{cleanupConfig.nextRun
|
||||||
|
? formatDate(cleanupConfig.nextRun)
|
||||||
|
: "Calculating..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-blue-50/50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-900">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||||
|
Background Operations
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-blue-800 dark:text-blue-200/80">
|
||||||
|
These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance.
|
||||||
|
Choose intervals that match your workflow and repository update frequency.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import { GitHubConfigForm } from './GitHubConfigForm';
|
import { GitHubConfigForm } from './GitHubConfigForm';
|
||||||
import { GiteaConfigForm } from './GiteaConfigForm';
|
import { GiteaConfigForm } from './GiteaConfigForm';
|
||||||
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
import { AutomationSettings } from './AutomationSettings';
|
||||||
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
|
||||||
import type {
|
import type {
|
||||||
ConfigApiResponse,
|
ConfigApiResponse,
|
||||||
GiteaConfig,
|
GiteaConfig,
|
||||||
@@ -11,6 +10,8 @@ import type {
|
|||||||
SaveConfigApiResponse,
|
SaveConfigApiResponse,
|
||||||
ScheduleConfig,
|
ScheduleConfig,
|
||||||
DatabaseCleanupConfig,
|
DatabaseCleanupConfig,
|
||||||
|
MirrorOptions,
|
||||||
|
AdvancedOptions,
|
||||||
} from '@/types/config';
|
} from '@/types/config';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
@@ -25,6 +26,8 @@ type ConfigState = {
|
|||||||
giteaConfig: GiteaConfig;
|
giteaConfig: GiteaConfig;
|
||||||
scheduleConfig: ScheduleConfig;
|
scheduleConfig: ScheduleConfig;
|
||||||
cleanupConfig: DatabaseCleanupConfig;
|
cleanupConfig: DatabaseCleanupConfig;
|
||||||
|
mirrorOptions: MirrorOptions;
|
||||||
|
advancedOptions: AdvancedOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ConfigTabs() {
|
export function ConfigTabs() {
|
||||||
@@ -32,12 +35,8 @@ export function ConfigTabs() {
|
|||||||
githubConfig: {
|
githubConfig: {
|
||||||
username: '',
|
username: '',
|
||||||
token: '',
|
token: '',
|
||||||
skipForks: false,
|
|
||||||
privateRepositories: false,
|
privateRepositories: false,
|
||||||
mirrorIssues: false,
|
|
||||||
mirrorStarred: false,
|
mirrorStarred: false,
|
||||||
preserveOrgStructure: false,
|
|
||||||
skipStarredIssues: false,
|
|
||||||
},
|
},
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
url: '',
|
url: '',
|
||||||
@@ -46,6 +45,7 @@ export function ConfigTabs() {
|
|||||||
organization: 'github-mirrors',
|
organization: 'github-mirrors',
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
starredReposOrg: 'github',
|
starredReposOrg: 'github',
|
||||||
|
preserveOrgStructure: false,
|
||||||
},
|
},
|
||||||
scheduleConfig: {
|
scheduleConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -55,15 +55,34 @@ export function ConfigTabs() {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
retentionDays: 604800, // 7 days in seconds
|
retentionDays: 604800, // 7 days in seconds
|
||||||
},
|
},
|
||||||
|
mirrorOptions: {
|
||||||
|
mirrorReleases: false,
|
||||||
|
mirrorMetadata: false,
|
||||||
|
metadataComponents: {
|
||||||
|
issues: false,
|
||||||
|
pullRequests: false,
|
||||||
|
labels: false,
|
||||||
|
milestones: false,
|
||||||
|
wiki: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
advancedOptions: {
|
||||||
|
skipForks: false,
|
||||||
|
skipStarredIssues: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const { user, refreshUser } = useAuth();
|
const { user } = useAuth();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
const [isSyncing, setIsSyncing] = useState<boolean>(false);
|
||||||
const [isConfigSaved, setIsConfigSaved] = useState<boolean>(false);
|
|
||||||
const [isAutoSavingSchedule, setIsAutoSavingSchedule] = useState<boolean>(false);
|
const [isAutoSavingSchedule, setIsAutoSavingSchedule] = useState<boolean>(false);
|
||||||
const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState<boolean>(false);
|
const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState<boolean>(false);
|
||||||
|
const [isAutoSavingGitHub, setIsAutoSavingGitHub] = useState<boolean>(false);
|
||||||
|
const [isAutoSavingGitea, setIsAutoSavingGitea] = useState<boolean>(false);
|
||||||
const autoSaveScheduleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const autoSaveScheduleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const autoSaveGitHubTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const autoSaveGiteaTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const isConfigFormValid = (): boolean => {
|
const isConfigFormValid = (): boolean => {
|
||||||
const { githubConfig, giteaConfig } = config;
|
const { githubConfig, giteaConfig } = config;
|
||||||
@@ -78,6 +97,11 @@ export function ConfigTabs() {
|
|||||||
return isGitHubValid && isGiteaValid;
|
return isGitHubValid && isGiteaValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isGitHubConfigValid = (): boolean => {
|
||||||
|
const { githubConfig } = config;
|
||||||
|
return !!(githubConfig.username.trim() && githubConfig.token.trim());
|
||||||
|
};
|
||||||
|
|
||||||
// Removed the problematic useEffect that was causing circular dependencies
|
// Removed the problematic useEffect that was causing circular dependencies
|
||||||
// The lastRun and nextRun should be managed by the backend and fetched via API
|
// The lastRun and nextRun should be managed by the backend and fetched via API
|
||||||
|
|
||||||
@@ -109,44 +133,9 @@ export function ConfigTabs() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveConfig = async () => {
|
|
||||||
if (!user?.id) return;
|
|
||||||
const reqPayload: SaveConfigApiRequest = {
|
|
||||||
userId: user.id,
|
|
||||||
githubConfig: config.githubConfig,
|
|
||||||
giteaConfig: config.giteaConfig,
|
|
||||||
scheduleConfig: config.scheduleConfig,
|
|
||||||
cleanupConfig: config.cleanupConfig,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(reqPayload),
|
|
||||||
});
|
|
||||||
const result: SaveConfigApiResponse = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
await refreshUser();
|
|
||||||
setIsConfigSaved(true);
|
|
||||||
// Invalidate config cache so other components get fresh data
|
|
||||||
invalidateConfigCache();
|
|
||||||
toast.success(
|
|
||||||
'Configuration saved successfully! Now import your GitHub data to begin.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showErrorToast(
|
|
||||||
`Failed to save configuration: ${result.message || 'Unknown error'}`,
|
|
||||||
toast
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showErrorToast(error, toast);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-save function specifically for schedule config changes
|
// Auto-save function specifically for schedule config changes
|
||||||
const autoSaveScheduleConfig = useCallback(async (scheduleConfig: ScheduleConfig) => {
|
const autoSaveScheduleConfig = useCallback(async (scheduleConfig: ScheduleConfig) => {
|
||||||
if (!user?.id || !isConfigSaved) return; // Only auto-save if config was previously saved
|
if (!user?.id) return;
|
||||||
|
|
||||||
// Clear any existing timeout
|
// Clear any existing timeout
|
||||||
if (autoSaveScheduleTimeoutRef.current) {
|
if (autoSaveScheduleTimeoutRef.current) {
|
||||||
@@ -163,6 +152,8 @@ export function ConfigTabs() {
|
|||||||
giteaConfig: config.giteaConfig,
|
giteaConfig: config.giteaConfig,
|
||||||
scheduleConfig: scheduleConfig,
|
scheduleConfig: scheduleConfig,
|
||||||
cleanupConfig: config.cleanupConfig,
|
cleanupConfig: config.cleanupConfig,
|
||||||
|
mirrorOptions: config.mirrorOptions,
|
||||||
|
advancedOptions: config.advancedOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -206,11 +197,11 @@ export function ConfigTabs() {
|
|||||||
setIsAutoSavingSchedule(false);
|
setIsAutoSavingSchedule(false);
|
||||||
}
|
}
|
||||||
}, 500); // 500ms debounce
|
}, 500); // 500ms debounce
|
||||||
}, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig, config.cleanupConfig]);
|
}, [user?.id, config.githubConfig, config.giteaConfig, config.cleanupConfig]);
|
||||||
|
|
||||||
// Auto-save function specifically for cleanup config changes
|
// Auto-save function specifically for cleanup config changes
|
||||||
const autoSaveCleanupConfig = useCallback(async (cleanupConfig: DatabaseCleanupConfig) => {
|
const autoSaveCleanupConfig = useCallback(async (cleanupConfig: DatabaseCleanupConfig) => {
|
||||||
if (!user?.id || !isConfigSaved) return; // Only auto-save if config was previously saved
|
if (!user?.id) return;
|
||||||
|
|
||||||
// Clear any existing timeout
|
// Clear any existing timeout
|
||||||
if (autoSaveCleanupTimeoutRef.current) {
|
if (autoSaveCleanupTimeoutRef.current) {
|
||||||
@@ -227,6 +218,8 @@ export function ConfigTabs() {
|
|||||||
giteaConfig: config.giteaConfig,
|
giteaConfig: config.giteaConfig,
|
||||||
scheduleConfig: config.scheduleConfig,
|
scheduleConfig: config.scheduleConfig,
|
||||||
cleanupConfig: cleanupConfig,
|
cleanupConfig: cleanupConfig,
|
||||||
|
mirrorOptions: config.mirrorOptions,
|
||||||
|
advancedOptions: config.advancedOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -269,7 +262,175 @@ export function ConfigTabs() {
|
|||||||
setIsAutoSavingCleanup(false);
|
setIsAutoSavingCleanup(false);
|
||||||
}
|
}
|
||||||
}, 500); // 500ms debounce
|
}, 500); // 500ms debounce
|
||||||
}, [user?.id, isConfigSaved, config.githubConfig, config.giteaConfig, config.scheduleConfig]);
|
}, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig]);
|
||||||
|
|
||||||
|
// Auto-save function specifically for GitHub config changes
|
||||||
|
const autoSaveGitHubConfig = useCallback(async (githubConfig: GitHubConfig) => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (autoSaveGitHubTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveGitHubTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce the auto-save to prevent excessive API calls
|
||||||
|
autoSaveGitHubTimeoutRef.current = setTimeout(async () => {
|
||||||
|
setIsAutoSavingGitHub(true);
|
||||||
|
|
||||||
|
const reqPayload: SaveConfigApiRequest = {
|
||||||
|
userId: user.id!,
|
||||||
|
githubConfig: githubConfig,
|
||||||
|
giteaConfig: config.giteaConfig,
|
||||||
|
scheduleConfig: config.scheduleConfig,
|
||||||
|
cleanupConfig: config.cleanupConfig,
|
||||||
|
mirrorOptions: config.mirrorOptions,
|
||||||
|
advancedOptions: config.advancedOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(reqPayload),
|
||||||
|
});
|
||||||
|
const result: SaveConfigApiResponse = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Silent success - no toast for auto-save
|
||||||
|
// Invalidate config cache so other components get fresh data
|
||||||
|
invalidateConfigCache();
|
||||||
|
} else {
|
||||||
|
showErrorToast(
|
||||||
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
|
toast
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
} finally {
|
||||||
|
setIsAutoSavingGitHub(false);
|
||||||
|
}
|
||||||
|
}, 500); // 500ms debounce
|
||||||
|
}, [user?.id, config.giteaConfig, config.scheduleConfig, config.cleanupConfig]);
|
||||||
|
|
||||||
|
// Auto-save function specifically for Gitea config changes
|
||||||
|
const autoSaveGiteaConfig = useCallback(async (giteaConfig: GiteaConfig) => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (autoSaveGiteaTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveGiteaTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce the auto-save to prevent excessive API calls
|
||||||
|
autoSaveGiteaTimeoutRef.current = setTimeout(async () => {
|
||||||
|
setIsAutoSavingGitea(true);
|
||||||
|
|
||||||
|
const reqPayload: SaveConfigApiRequest = {
|
||||||
|
userId: user.id!,
|
||||||
|
githubConfig: config.githubConfig,
|
||||||
|
giteaConfig: giteaConfig,
|
||||||
|
scheduleConfig: config.scheduleConfig,
|
||||||
|
cleanupConfig: config.cleanupConfig,
|
||||||
|
mirrorOptions: config.mirrorOptions,
|
||||||
|
advancedOptions: config.advancedOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(reqPayload),
|
||||||
|
});
|
||||||
|
const result: SaveConfigApiResponse = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Silent success - no toast for auto-save
|
||||||
|
// Invalidate config cache so other components get fresh data
|
||||||
|
invalidateConfigCache();
|
||||||
|
} else {
|
||||||
|
showErrorToast(
|
||||||
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
|
toast
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
} finally {
|
||||||
|
setIsAutoSavingGitea(false);
|
||||||
|
}
|
||||||
|
}, 500); // 500ms debounce
|
||||||
|
}, [user?.id, config.githubConfig, config.scheduleConfig, config.cleanupConfig]);
|
||||||
|
|
||||||
|
// Auto-save function for mirror options (handled within GitHub config)
|
||||||
|
const autoSaveMirrorOptions = useCallback(async (mirrorOptions: MirrorOptions) => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
const reqPayload: SaveConfigApiRequest = {
|
||||||
|
userId: user.id!,
|
||||||
|
githubConfig: config.githubConfig,
|
||||||
|
giteaConfig: config.giteaConfig,
|
||||||
|
scheduleConfig: config.scheduleConfig,
|
||||||
|
cleanupConfig: config.cleanupConfig,
|
||||||
|
mirrorOptions: mirrorOptions,
|
||||||
|
advancedOptions: config.advancedOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(reqPayload),
|
||||||
|
});
|
||||||
|
const result: SaveConfigApiResponse = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
invalidateConfigCache();
|
||||||
|
} else {
|
||||||
|
showErrorToast(
|
||||||
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
|
toast
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
}
|
||||||
|
}, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.advancedOptions]);
|
||||||
|
|
||||||
|
// Auto-save function for advanced options (handled within GitHub config)
|
||||||
|
const autoSaveAdvancedOptions = useCallback(async (advancedOptions: AdvancedOptions) => {
|
||||||
|
if (!user?.id) return;
|
||||||
|
|
||||||
|
const reqPayload: SaveConfigApiRequest = {
|
||||||
|
userId: user.id!,
|
||||||
|
githubConfig: config.githubConfig,
|
||||||
|
giteaConfig: config.giteaConfig,
|
||||||
|
scheduleConfig: config.scheduleConfig,
|
||||||
|
cleanupConfig: config.cleanupConfig,
|
||||||
|
mirrorOptions: config.mirrorOptions,
|
||||||
|
advancedOptions: advancedOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(reqPayload),
|
||||||
|
});
|
||||||
|
const result: SaveConfigApiResponse = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
invalidateConfigCache();
|
||||||
|
} else {
|
||||||
|
showErrorToast(
|
||||||
|
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||||
|
toast
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
}
|
||||||
|
}, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.mirrorOptions]);
|
||||||
|
|
||||||
// Cleanup timeouts on unmount
|
// Cleanup timeouts on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -280,6 +441,12 @@ export function ConfigTabs() {
|
|||||||
if (autoSaveCleanupTimeoutRef.current) {
|
if (autoSaveCleanupTimeoutRef.current) {
|
||||||
clearTimeout(autoSaveCleanupTimeoutRef.current);
|
clearTimeout(autoSaveCleanupTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
if (autoSaveGitHubTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveGitHubTimeoutRef.current);
|
||||||
|
}
|
||||||
|
if (autoSaveGiteaTimeoutRef.current) {
|
||||||
|
clearTimeout(autoSaveGiteaTimeoutRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -303,8 +470,12 @@ export function ConfigTabs() {
|
|||||||
response.scheduleConfig || config.scheduleConfig,
|
response.scheduleConfig || config.scheduleConfig,
|
||||||
cleanupConfig:
|
cleanupConfig:
|
||||||
response.cleanupConfig || config.cleanupConfig,
|
response.cleanupConfig || config.cleanupConfig,
|
||||||
|
mirrorOptions:
|
||||||
|
response.mirrorOptions || config.mirrorOptions,
|
||||||
|
advancedOptions:
|
||||||
|
response.advancedOptions || config.advancedOptions,
|
||||||
});
|
});
|
||||||
if (response.id) setIsConfigSaved(true);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -329,14 +500,14 @@ export function ConfigTabs() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-4">
|
<div className="flex gap-x-4">
|
||||||
<Skeleton className="h-10 w-36" />
|
<Skeleton className="h-10 w-36" />
|
||||||
<Skeleton className="h-10 w-36" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content section */}
|
{/* Content section - Grid layout */}
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex gap-x-4">
|
{/* GitHub & Gitea connections - Side by side */}
|
||||||
<div className="w-1/2 border rounded-lg p-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<Skeleton className="h-6 w-40" />
|
<Skeleton className="h-6 w-40" />
|
||||||
<Skeleton className="h-9 w-32" />
|
<Skeleton className="h-9 w-32" />
|
||||||
@@ -344,10 +515,13 @@ export function ConfigTabs() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Skeleton className="h-20 w-full" />
|
<Skeleton className="h-20 w-full" />
|
||||||
<Skeleton className="h-20 w-full" />
|
<Skeleton className="h-20 w-full" />
|
||||||
|
<Skeleton className="h-1 w-full" />
|
||||||
|
<Skeleton className="h-32 w-full" />
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
<Skeleton className="h-32 w-full" />
|
<Skeleton className="h-32 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2 border rounded-lg p-4">
|
<div className="border rounded-lg p-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<Skeleton className="h-6 w-40" />
|
<Skeleton className="h-6 w-40" />
|
||||||
<Skeleton className="h-9 w-32" />
|
<Skeleton className="h-9 w-32" />
|
||||||
@@ -356,23 +530,24 @@ export function ConfigTabs() {
|
|||||||
<Skeleton className="h-20 w-full" />
|
<Skeleton className="h-20 w-full" />
|
||||||
<Skeleton className="h-20 w-full" />
|
<Skeleton className="h-20 w-full" />
|
||||||
<Skeleton className="h-20 w-full" />
|
<Skeleton className="h-20 w-full" />
|
||||||
<Skeleton className="h-20 w-full" />
|
<Skeleton className="h-64 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-4">
|
|
||||||
<div className="w-1/2 border rounded-lg p-4">
|
{/* Automation & Maintenance - Full width */}
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<Skeleton className="h-8 w-48 mb-4" />
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Skeleton className="h-8 w-48" />
|
<Skeleton className="h-6 w-40" />
|
||||||
<Skeleton className="h-16 w-full" />
|
<Skeleton className="h-16 w-full" />
|
||||||
<Skeleton className="h-8 w-32" />
|
<Skeleton className="h-24 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="w-1/2 border rounded-lg p-4">
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Skeleton className="h-8 w-48" />
|
<Skeleton className="h-6 w-40" />
|
||||||
<Skeleton className="h-16 w-full" />
|
<Skeleton className="h-16 w-full" />
|
||||||
<Skeleton className="h-8 w-32" />
|
<Skeleton className="h-24 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,10 +576,10 @@ export function ConfigTabs() {
|
|||||||
<div className="flex gap-x-4">
|
<div className="flex gap-x-4">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleImportGitHubData}
|
onClick={handleImportGitHubData}
|
||||||
disabled={isSyncing || !isConfigSaved}
|
disabled={isSyncing || !isGitHubConfigValid()}
|
||||||
title={
|
title={
|
||||||
!isConfigSaved
|
!isGitHubConfigValid()
|
||||||
? 'Save configuration first'
|
? 'Please fill GitHub username and token fields'
|
||||||
: isSyncing
|
: isSyncing
|
||||||
? 'Import in progress'
|
? 'Import in progress'
|
||||||
: 'Import GitHub Data'
|
: 'Import GitHub Data'
|
||||||
@@ -422,23 +597,13 @@ export function ConfigTabs() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
onClick={handleSaveConfig}
|
|
||||||
disabled={!isConfigFormValid()}
|
|
||||||
title={
|
|
||||||
!isConfigFormValid()
|
|
||||||
? 'Please fill all required fields'
|
|
||||||
: 'Save Configuration'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Save Configuration
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content section */}
|
{/* Content section - Grid layout */}
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="space-y-6">
|
||||||
<div className="flex gap-x-4">
|
{/* GitHub & Gitea connections - Side by side */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:items-stretch">
|
||||||
<GitHubConfigForm
|
<GitHubConfigForm
|
||||||
config={config.githubConfig}
|
config={config.githubConfig}
|
||||||
setConfig={update =>
|
setConfig={update =>
|
||||||
@@ -450,6 +615,30 @@ export function ConfigTabs() {
|
|||||||
: update,
|
: update,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
mirrorOptions={config.mirrorOptions}
|
||||||
|
setMirrorOptions={update =>
|
||||||
|
setConfig(prev => ({
|
||||||
|
...prev,
|
||||||
|
mirrorOptions:
|
||||||
|
typeof update === 'function'
|
||||||
|
? update(prev.mirrorOptions)
|
||||||
|
: update,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
advancedOptions={config.advancedOptions}
|
||||||
|
setAdvancedOptions={update =>
|
||||||
|
setConfig(prev => ({
|
||||||
|
...prev,
|
||||||
|
advancedOptions:
|
||||||
|
typeof update === 'function'
|
||||||
|
? update(prev.advancedOptions)
|
||||||
|
: update,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onAutoSave={autoSaveGitHubConfig}
|
||||||
|
onMirrorOptionsAutoSave={autoSaveMirrorOptions}
|
||||||
|
onAdvancedOptionsAutoSave={autoSaveAdvancedOptions}
|
||||||
|
isAutoSaving={isAutoSavingGitHub}
|
||||||
/>
|
/>
|
||||||
<GiteaConfigForm
|
<GiteaConfigForm
|
||||||
config={config.giteaConfig}
|
config={config.giteaConfig}
|
||||||
@@ -462,41 +651,28 @@ export function ConfigTabs() {
|
|||||||
: update,
|
: update,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
onAutoSave={autoSaveGiteaConfig}
|
||||||
|
isAutoSaving={isAutoSavingGitea}
|
||||||
|
githubUsername={config.githubConfig.username}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-4">
|
|
||||||
<div className="w-1/2">
|
{/* Automation & Maintenance - Full width */}
|
||||||
<ScheduleConfigForm
|
<div>
|
||||||
config={config.scheduleConfig}
|
<AutomationSettings
|
||||||
setConfig={update =>
|
scheduleConfig={config.scheduleConfig}
|
||||||
setConfig(prev => ({
|
cleanupConfig={config.cleanupConfig}
|
||||||
...prev,
|
onScheduleChange={(newConfig) => {
|
||||||
scheduleConfig:
|
setConfig(prev => ({ ...prev, scheduleConfig: newConfig }));
|
||||||
typeof update === 'function'
|
autoSaveScheduleConfig(newConfig);
|
||||||
? update(prev.scheduleConfig)
|
}}
|
||||||
: update,
|
onCleanupChange={(newConfig) => {
|
||||||
}))
|
setConfig(prev => ({ ...prev, cleanupConfig: newConfig }));
|
||||||
}
|
autoSaveCleanupConfig(newConfig);
|
||||||
onAutoSave={autoSaveScheduleConfig}
|
}}
|
||||||
isAutoSaving={isAutoSavingSchedule}
|
isAutoSavingSchedule={isAutoSavingSchedule}
|
||||||
/>
|
isAutoSavingCleanup={isAutoSavingCleanup}
|
||||||
</div>
|
/>
|
||||||
<div className="w-1/2">
|
|
||||||
<DatabaseCleanupConfigForm
|
|
||||||
config={config.cleanupConfig}
|
|
||||||
setConfig={update =>
|
|
||||||
setConfig(prev => ({
|
|
||||||
...prev,
|
|
||||||
cleanupConfig:
|
|
||||||
typeof update === 'function'
|
|
||||||
? update(prev.cleanupConfig)
|
|
||||||
: update,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onAutoSave={autoSaveCleanupConfig}
|
|
||||||
isAutoSaving={isAutoSavingCleanup}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function DatabaseCleanupConfigForm({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="self-start">
|
||||||
<CardContent className="pt-6 relative">
|
<CardContent className="pt-6 relative">
|
||||||
{isAutoSaving && (
|
{isAutoSaving && (
|
||||||
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
|
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -1,52 +1,65 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { githubApi } from "@/lib/api";
|
import { githubApi } from "@/lib/api";
|
||||||
import type { GitHubConfig } from "@/types/config";
|
import type { GitHubConfig, MirrorOptions, AdvancedOptions } from "@/types/config";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Checkbox } from "../ui/checkbox";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { Alert, AlertDescription } from "../ui/alert";
|
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { GitHubMirrorSettings } from "./GitHubMirrorSettings";
|
||||||
|
import { Separator } from "../ui/separator";
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from "@/components/ui/hover-card";
|
||||||
|
|
||||||
interface GitHubConfigFormProps {
|
interface GitHubConfigFormProps {
|
||||||
config: GitHubConfig;
|
config: GitHubConfig;
|
||||||
setConfig: React.Dispatch<React.SetStateAction<GitHubConfig>>;
|
setConfig: React.Dispatch<React.SetStateAction<GitHubConfig>>;
|
||||||
|
mirrorOptions: MirrorOptions;
|
||||||
|
setMirrorOptions: React.Dispatch<React.SetStateAction<MirrorOptions>>;
|
||||||
|
advancedOptions: AdvancedOptions;
|
||||||
|
setAdvancedOptions: React.Dispatch<React.SetStateAction<AdvancedOptions>>;
|
||||||
|
onAutoSave?: (githubConfig: GitHubConfig) => Promise<void>;
|
||||||
|
onMirrorOptionsAutoSave?: (mirrorOptions: MirrorOptions) => Promise<void>;
|
||||||
|
onAdvancedOptionsAutoSave?: (advancedOptions: AdvancedOptions) => Promise<void>;
|
||||||
|
isAutoSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) {
|
export function GitHubConfigForm({
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
mirrorOptions,
|
||||||
|
setMirrorOptions,
|
||||||
|
advancedOptions,
|
||||||
|
setAdvancedOptions,
|
||||||
|
onAutoSave,
|
||||||
|
onMirrorOptionsAutoSave,
|
||||||
|
onAdvancedOptionsAutoSave,
|
||||||
|
isAutoSaving
|
||||||
|
}: GitHubConfigFormProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value, type, checked } = e.target;
|
const { name, value, type, checked } = e.target;
|
||||||
|
|
||||||
// Special handling for preserveOrgStructure changes
|
const newConfig = {
|
||||||
if (
|
|
||||||
name === "preserveOrgStructure" &&
|
|
||||||
config.preserveOrgStructure !== checked
|
|
||||||
) {
|
|
||||||
toast.info(
|
|
||||||
"Changing this setting may affect how repositories are accessed in Gitea. " +
|
|
||||||
"Existing mirrored repositories will still be accessible during sync operations.",
|
|
||||||
{
|
|
||||||
duration: 6000,
|
|
||||||
position: "top-center",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig({
|
|
||||||
...config,
|
...config,
|
||||||
[name]: type === "checkbox" ? checked : value,
|
[name]: type === "checkbox" ? checked : value,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
|
// Auto-save for all field changes
|
||||||
|
if (onAutoSave) {
|
||||||
|
onAutoSave(newConfig);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testConnection = async () => {
|
const testConnection = async () => {
|
||||||
@@ -74,7 +87,7 @@ export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full h-full flex flex-col">
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
||||||
<CardTitle className="text-lg font-semibold">
|
<CardTitle className="text-lg font-semibold">
|
||||||
GitHub Configuration
|
GitHub Configuration
|
||||||
@@ -89,7 +102,7 @@ export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-y-6">
|
<CardContent className="flex flex-col gap-y-6 flex-1">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="github-username"
|
htmlFor="github-username"
|
||||||
@@ -110,12 +123,49 @@ export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<div className="flex items-center gap-2 mb-1.5">
|
||||||
htmlFor="github-token"
|
<label
|
||||||
className="block text-sm font-medium mb-1.5"
|
htmlFor="github-token"
|
||||||
>
|
className="block text-sm font-medium"
|
||||||
GitHub Token
|
>
|
||||||
</label>
|
GitHub Token
|
||||||
|
</label>
|
||||||
|
<HoverCard openDelay={200}>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<span className="inline-flex p-0.5 hover:bg-muted rounded-sm transition-colors cursor-help">
|
||||||
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</span>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent side="right" align="start" className="w-80">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium text-sm">GitHub Token Requirements</h4>
|
||||||
|
<div className="text-sm space-y-2">
|
||||||
|
<p>
|
||||||
|
You need to create a <span className="font-medium">Classic GitHub PAT Token</span> with the following scopes:
|
||||||
|
</p>
|
||||||
|
<ul className="ml-4 space-y-1 list-disc">
|
||||||
|
<li><code className="text-xs bg-muted px-1 py-0.5 rounded">repo</code></li>
|
||||||
|
<li><code className="text-xs bg-muted px-1 py-0.5 rounded">admin:org</code></li>
|
||||||
|
</ul>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
The organization access is required for mirroring organization repositories.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Generate tokens at{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/tokens"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-primary hover:underline font-medium"
|
||||||
|
>
|
||||||
|
github.com/settings/tokens
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
id="github-token"
|
id="github-token"
|
||||||
name="token"
|
name="token"
|
||||||
@@ -123,7 +173,7 @@ export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) {
|
|||||||
value={config.token}
|
value={config.token}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
placeholder="Your GitHub personal access token"
|
placeholder="Your GitHub token (classic) with repo and admin:org scopes"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Required for private repositories, organizations, and starred
|
Required for private repositories, organizations, and starred
|
||||||
@@ -131,210 +181,27 @@ export function GitHubConfigForm({ config, setConfig }: GitHubConfigFormProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<Separator />
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="skip-forks"
|
|
||||||
name="skipForks"
|
|
||||||
checked={config.skipForks}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
name: "skipForks",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: Boolean(checked),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="skip-forks"
|
|
||||||
className="ml-2 block text-sm select-none"
|
|
||||||
>
|
|
||||||
Skip Forks
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
<GitHubMirrorSettings
|
||||||
<Checkbox
|
githubConfig={config}
|
||||||
id="private-repositories"
|
mirrorOptions={mirrorOptions}
|
||||||
name="privateRepositories"
|
advancedOptions={advancedOptions}
|
||||||
checked={config.privateRepositories}
|
onGitHubConfigChange={(newConfig) => {
|
||||||
onCheckedChange={(checked) =>
|
setConfig(newConfig);
|
||||||
handleChange({
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
target: {
|
}}
|
||||||
name: "privateRepositories",
|
onMirrorOptionsChange={(newOptions) => {
|
||||||
type: "checkbox",
|
setMirrorOptions(newOptions);
|
||||||
checked: Boolean(checked),
|
if (onMirrorOptionsAutoSave) onMirrorOptionsAutoSave(newOptions);
|
||||||
value: "",
|
}}
|
||||||
},
|
onAdvancedOptionsChange={(newOptions) => {
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
setAdvancedOptions(newOptions);
|
||||||
}
|
if (onAdvancedOptionsAutoSave) onAdvancedOptionsAutoSave(newOptions);
|
||||||
/>
|
}}
|
||||||
<label
|
/>
|
||||||
htmlFor="private-repositories"
|
</CardContent>
|
||||||
className="ml-2 block text-sm select-none"
|
|
||||||
>
|
|
||||||
Mirror Private Repos
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="mirror-starred"
|
|
||||||
name="mirrorStarred"
|
|
||||||
checked={config.mirrorStarred}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
name: "mirrorStarred",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: Boolean(checked),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="mirror-starred"
|
|
||||||
className="ml-2 block text-sm select-none"
|
|
||||||
>
|
|
||||||
Mirror Starred Repos
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="mirror-issues"
|
|
||||||
name="mirrorIssues"
|
|
||||||
checked={config.mirrorIssues}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
name: "mirrorIssues",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: Boolean(checked),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="mirror-issues"
|
|
||||||
className="ml-2 block text-sm select-none"
|
|
||||||
>
|
|
||||||
Mirror Issues
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="preserve-org-structure"
|
|
||||||
name="preserveOrgStructure"
|
|
||||||
checked={config.preserveOrgStructure}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
name: "preserveOrgStructure",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: Boolean(checked),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="preserve-org-structure"
|
|
||||||
className="ml-2 text-sm select-none flex items-center"
|
|
||||||
>
|
|
||||||
Preserve Org Structure
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span
|
|
||||||
className="ml-1 cursor-pointer align-middle text-muted-foreground"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<Info size={16} />
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" className="max-w-xs text-xs">
|
|
||||||
When enabled, organization repositories will be mirrored to
|
|
||||||
the same organization structure in Gitea. When disabled, all
|
|
||||||
repositories will be mirrored under your Gitea username.
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="skip-starred-issues"
|
|
||||||
name="skipStarredIssues"
|
|
||||||
checked={config.skipStarredIssues}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleChange({
|
|
||||||
target: {
|
|
||||||
name: "skipStarredIssues",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: Boolean(checked),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="skip-starred-issues"
|
|
||||||
className="ml-2 block text-sm select-none"
|
|
||||||
>
|
|
||||||
Skip Issues for Starred Repos
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className="flex-col items-start">
|
|
||||||
<Alert variant="note" className="w-full">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400 mr-2" />
|
|
||||||
<AlertDescription className="text-sm">
|
|
||||||
<div className="font-semibold mb-1">Note:</div>
|
|
||||||
<div className="mb-1">
|
|
||||||
You need to create a{" "}
|
|
||||||
<span className="font-semibold">Classic GitHub PAT Token</span>{" "}
|
|
||||||
with following scopes:
|
|
||||||
</div>
|
|
||||||
<ul className="ml-4 mb-1 list-disc">
|
|
||||||
<li>
|
|
||||||
<code>repo</code>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>admin:org</code>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div className="mb-1">
|
|
||||||
The organization access is required for mirroring organization
|
|
||||||
repositories.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
You can generate tokens at{" "}
|
|
||||||
<a
|
|
||||||
href="https://github.com/settings/tokens"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="underline font-medium hover:text-blue-900 dark:hover:text-blue-200"
|
|
||||||
>
|
|
||||||
github.com/settings/tokens
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
527
src/components/config/GitHubMirrorSettings.tsx
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Info,
|
||||||
|
GitBranch,
|
||||||
|
Star,
|
||||||
|
Lock,
|
||||||
|
Archive,
|
||||||
|
GitPullRequest,
|
||||||
|
Tag,
|
||||||
|
FileText,
|
||||||
|
MessageSquare,
|
||||||
|
Target,
|
||||||
|
BookOpen,
|
||||||
|
GitFork,
|
||||||
|
ChevronDown,
|
||||||
|
Funnel
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { GitHubConfig, MirrorOptions, AdvancedOptions } from "@/types/config";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface GitHubMirrorSettingsProps {
|
||||||
|
githubConfig: GitHubConfig;
|
||||||
|
mirrorOptions: MirrorOptions;
|
||||||
|
advancedOptions: AdvancedOptions;
|
||||||
|
onGitHubConfigChange: (config: GitHubConfig) => void;
|
||||||
|
onMirrorOptionsChange: (options: MirrorOptions) => void;
|
||||||
|
onAdvancedOptionsChange: (options: AdvancedOptions) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GitHubMirrorSettings({
|
||||||
|
githubConfig,
|
||||||
|
mirrorOptions,
|
||||||
|
advancedOptions,
|
||||||
|
onGitHubConfigChange,
|
||||||
|
onMirrorOptionsChange,
|
||||||
|
onAdvancedOptionsChange,
|
||||||
|
}: GitHubMirrorSettingsProps) {
|
||||||
|
|
||||||
|
const handleGitHubChange = (field: keyof GitHubConfig, value: boolean) => {
|
||||||
|
onGitHubConfigChange({ ...githubConfig, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMirrorChange = (field: keyof MirrorOptions, value: boolean) => {
|
||||||
|
onMirrorOptionsChange({ ...mirrorOptions, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMetadataComponentChange = (component: keyof MirrorOptions['metadataComponents'], value: boolean) => {
|
||||||
|
onMirrorOptionsChange({
|
||||||
|
...mirrorOptions,
|
||||||
|
metadataComponents: {
|
||||||
|
...mirrorOptions.metadataComponents,
|
||||||
|
[component]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdvancedChange = (field: keyof AdvancedOptions, value: boolean) => {
|
||||||
|
onAdvancedOptionsChange({ ...advancedOptions, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
// When metadata is disabled, all components should be disabled
|
||||||
|
const isMetadataEnabled = mirrorOptions.mirrorMetadata;
|
||||||
|
|
||||||
|
// Calculate what content is included for starred repos
|
||||||
|
const starredRepoContent = {
|
||||||
|
code: true, // Always included
|
||||||
|
releases: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorReleases,
|
||||||
|
issues: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||||
|
pullRequests: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||||
|
wiki: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||||
|
};
|
||||||
|
|
||||||
|
const starredContentCount = Object.entries(starredRepoContent).filter(([key, value]) => key !== 'code' && value).length;
|
||||||
|
const totalStarredOptions = 4; // releases, issues, PRs, wiki
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Repository Selection Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
|
<GitBranch className="h-4 w-4" />
|
||||||
|
Repository Selection
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Choose which repositories to include in mirroring
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="private-repos"
|
||||||
|
checked={githubConfig.privateRepositories}
|
||||||
|
onCheckedChange={(checked) => handleGitHubChange('privateRepositories', !!checked)}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="private-repos"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Lock className="h-3.5 w-3.5" />
|
||||||
|
Include private repositories
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Mirror your private repositories
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="starred-repos"
|
||||||
|
checked={githubConfig.mirrorStarred}
|
||||||
|
onCheckedChange={(checked) => handleGitHubChange('mirrorStarred', !!checked)}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="starred-repos"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Star className="h-3.5 w-3.5" />
|
||||||
|
Mirror starred repositories
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Include repositories you've starred on GitHub
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Starred repos content selection - inline to prevent layout shift */}
|
||||||
|
<div className={cn(
|
||||||
|
"flex items-center justify-end transition-opacity duration-200",
|
||||||
|
githubConfig.mirrorStarred ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||||
|
)}>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!githubConfig.mirrorStarred}
|
||||||
|
className="h-8 text-xs font-normal min-w-[140px] justify-between"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{advancedOptions.skipStarredIssues ? (
|
||||||
|
"Code only"
|
||||||
|
) : starredContentCount === 0 ? (
|
||||||
|
"Code only"
|
||||||
|
) : starredContentCount === totalStarredOptions ? (
|
||||||
|
"Full content"
|
||||||
|
) : (
|
||||||
|
`${starredContentCount + 1} of ${totalStarredOptions + 1} selected`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="ml-2 h-3 w-3 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-72">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">Starred repos content</div>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="left" className="max-w-xs">
|
||||||
|
<p className="text-xs">
|
||||||
|
Choose what content to mirror from starred repositories.
|
||||||
|
Selecting "Lightweight mode" will only mirror code for better performance.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-2" />
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
|
<Checkbox
|
||||||
|
id="starred-lightweight"
|
||||||
|
checked={advancedOptions.skipStarredIssues}
|
||||||
|
onCheckedChange={(checked) => handleAdvancedChange('skipStarredIssues', !!checked)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="starred-lightweight"
|
||||||
|
className="text-sm font-normal cursor-pointer flex-1"
|
||||||
|
>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="font-medium">Lightweight mode</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
Only mirror code, skip all metadata
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!advancedOptions.skipStarredIssues && (
|
||||||
|
<>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">
|
||||||
|
Content included for starred repos:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2 text-xs pl-2">
|
||||||
|
<GitBranch className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span>Source code</span>
|
||||||
|
<Badge variant="secondary" className="ml-auto text-[10px] px-2 h-4">Always</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"flex items-center gap-2 text-xs pl-2",
|
||||||
|
starredRepoContent.releases ? "" : "opacity-50"
|
||||||
|
)}>
|
||||||
|
<Tag className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span>Releases & Tags</span>
|
||||||
|
{starredRepoContent.releases && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"flex items-center gap-2 text-xs pl-2",
|
||||||
|
starredRepoContent.issues ? "" : "opacity-50"
|
||||||
|
)}>
|
||||||
|
<MessageSquare className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span>Issues</span>
|
||||||
|
{starredRepoContent.issues && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"flex items-center gap-2 text-xs pl-2",
|
||||||
|
starredRepoContent.pullRequests ? "" : "opacity-50"
|
||||||
|
)}>
|
||||||
|
<GitPullRequest className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span>Pull Requests</span>
|
||||||
|
{starredRepoContent.pullRequests && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"flex items-center gap-2 text-xs pl-2",
|
||||||
|
starredRepoContent.wiki ? "" : "opacity-50"
|
||||||
|
)}>
|
||||||
|
<BookOpen className="h-3 w-3 text-muted-foreground" />
|
||||||
|
<span>Wiki</span>
|
||||||
|
{starredRepoContent.wiki && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-2">
|
||||||
|
To include more content, enable them in the Content & Data section below
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Content & Data Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
|
<Archive className="h-4 w-4" />
|
||||||
|
Content & Data
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Select what content to mirror from each repository
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Code is always mirrored - shown as info */}
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-muted/50 dark:bg-muted/20 rounded-md">
|
||||||
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm">Source code & branches</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Always included</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="text-xs">Default</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="mirror-releases"
|
||||||
|
checked={mirrorOptions.mirrorReleases}
|
||||||
|
onCheckedChange={(checked) => handleMirrorChange('mirrorReleases', !!checked)}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="mirror-releases"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Tag className="h-3.5 w-3.5" />
|
||||||
|
Releases & Tags
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Include GitHub releases, tags, and associated assets
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="mirror-metadata"
|
||||||
|
checked={mirrorOptions.mirrorMetadata}
|
||||||
|
onCheckedChange={(checked) => handleMirrorChange('mirrorMetadata', !!checked)}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="mirror-metadata"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
Repository Metadata
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Mirror issues, pull requests, and other repository data
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata multi-select - inline to prevent layout shift */}
|
||||||
|
<div className={cn(
|
||||||
|
"flex items-center justify-end transition-opacity duration-200",
|
||||||
|
mirrorOptions.mirrorMetadata ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||||
|
)}>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!mirrorOptions.mirrorMetadata}
|
||||||
|
className="h-8 text-xs font-normal min-w-[140px] justify-between"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{(() => {
|
||||||
|
const selectedCount = Object.values(mirrorOptions.metadataComponents).filter(Boolean).length;
|
||||||
|
const totalCount = Object.keys(mirrorOptions.metadataComponents).length;
|
||||||
|
if (selectedCount === 0) return "No items selected";
|
||||||
|
if (selectedCount === totalCount) return "All items selected";
|
||||||
|
return `${selectedCount} of ${totalCount} selected`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="ml-2 h-3 w-3 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-64">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm font-medium">Metadata to mirror</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto px-2 py-1 text-xs font-normal text-primary hover:text-primary/80"
|
||||||
|
onClick={() => {
|
||||||
|
const allSelected = Object.values(mirrorOptions.metadataComponents).every(Boolean);
|
||||||
|
const newValue = !allSelected;
|
||||||
|
|
||||||
|
// Update all metadata components at once
|
||||||
|
onMirrorOptionsChange({
|
||||||
|
...mirrorOptions,
|
||||||
|
metadataComponents: {
|
||||||
|
issues: newValue,
|
||||||
|
pullRequests: newValue,
|
||||||
|
labels: newValue,
|
||||||
|
milestones: newValue,
|
||||||
|
wiki: newValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.values(mirrorOptions.metadataComponents).every(Boolean) ? 'Deselect all' : 'Select all'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-2" />
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-issues-popup"
|
||||||
|
checked={mirrorOptions.metadataComponents.issues}
|
||||||
|
onCheckedChange={(checked) => handleMetadataComponentChange('issues', !!checked)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="metadata-issues-popup"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||||
|
>
|
||||||
|
<MessageSquare className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
Issues
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-prs-popup"
|
||||||
|
checked={mirrorOptions.metadataComponents.pullRequests}
|
||||||
|
onCheckedChange={(checked) => handleMetadataComponentChange('pullRequests', !!checked)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="metadata-prs-popup"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||||
|
>
|
||||||
|
<GitPullRequest className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
Pull Requests
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-labels-popup"
|
||||||
|
checked={mirrorOptions.metadataComponents.labels}
|
||||||
|
onCheckedChange={(checked) => handleMetadataComponentChange('labels', !!checked)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="metadata-labels-popup"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||||
|
>
|
||||||
|
<Tag className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
Labels
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-milestones-popup"
|
||||||
|
checked={mirrorOptions.metadataComponents.milestones}
|
||||||
|
onCheckedChange={(checked) => handleMetadataComponentChange('milestones', !!checked)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="metadata-milestones-popup"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||||
|
>
|
||||||
|
<Target className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
Milestones
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-wiki-popup"
|
||||||
|
checked={mirrorOptions.metadataComponents.wiki}
|
||||||
|
onCheckedChange={(checked) => handleMetadataComponentChange('wiki', !!checked)}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
htmlFor="metadata-wiki-popup"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||||
|
>
|
||||||
|
<BookOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
Wiki
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
{/* Filtering & Behavior Section */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
|
<Funnel className="h-4 w-4" />
|
||||||
|
Filtering & Behavior
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Fine-tune what gets excluded from mirroring
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<Checkbox
|
||||||
|
id="skip-forks"
|
||||||
|
checked={advancedOptions.skipForks}
|
||||||
|
onCheckedChange={(checked) => handleAdvancedChange('skipForks', !!checked)}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5 flex-1">
|
||||||
|
<Label
|
||||||
|
htmlFor="skip-forks"
|
||||||
|
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<GitFork className="h-3.5 w-3.5" />
|
||||||
|
Skip forked repositories
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Exclude repositories that are forks of other projects
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,39 +1,99 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "../ui/select";
|
|
||||||
import { giteaApi } from "@/lib/api";
|
import { giteaApi } from "@/lib/api";
|
||||||
import type { GiteaConfig, GiteaOrgVisibility } from "@/types/config";
|
import type { GiteaConfig, MirrorStrategy } from "@/types/config";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { OrganizationStrategy } from "./OrganizationStrategy";
|
||||||
|
import { OrganizationConfiguration } from "./OrganizationConfiguration";
|
||||||
|
import { Separator } from "../ui/separator";
|
||||||
|
|
||||||
interface GiteaConfigFormProps {
|
interface GiteaConfigFormProps {
|
||||||
config: GiteaConfig;
|
config: GiteaConfig;
|
||||||
setConfig: React.Dispatch<React.SetStateAction<GiteaConfig>>;
|
setConfig: React.Dispatch<React.SetStateAction<GiteaConfig>>;
|
||||||
|
onAutoSave?: (giteaConfig: GiteaConfig) => Promise<void>;
|
||||||
|
isAutoSaving?: boolean;
|
||||||
|
githubUsername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GiteaConfigForm({ config, setConfig }: GiteaConfigFormProps) {
|
export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, githubUsername }: GiteaConfigFormProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Derive the mirror strategy from existing config for backward compatibility
|
||||||
|
const getMirrorStrategy = (): MirrorStrategy => {
|
||||||
|
if (config.mirrorStrategy) return config.mirrorStrategy;
|
||||||
|
if (config.preserveOrgStructure) return "preserve";
|
||||||
|
if (config.organization && config.organization !== config.username) return "single-org";
|
||||||
|
return "flat-user";
|
||||||
|
};
|
||||||
|
|
||||||
|
const [mirrorStrategy, setMirrorStrategy] = useState<MirrorStrategy>(getMirrorStrategy());
|
||||||
|
|
||||||
|
// Update config when strategy changes
|
||||||
|
useEffect(() => {
|
||||||
|
const newConfig = { ...config };
|
||||||
|
|
||||||
|
switch (mirrorStrategy) {
|
||||||
|
case "preserve":
|
||||||
|
newConfig.preserveOrgStructure = true;
|
||||||
|
newConfig.mirrorStrategy = "preserve";
|
||||||
|
break;
|
||||||
|
case "single-org":
|
||||||
|
newConfig.preserveOrgStructure = false;
|
||||||
|
newConfig.mirrorStrategy = "single-org";
|
||||||
|
if (!newConfig.organization) {
|
||||||
|
newConfig.organization = "github-mirrors";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "flat-user":
|
||||||
|
newConfig.preserveOrgStructure = false;
|
||||||
|
newConfig.mirrorStrategy = "flat-user";
|
||||||
|
newConfig.organization = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(newConfig);
|
||||||
|
if (onAutoSave) {
|
||||||
|
onAutoSave(newConfig);
|
||||||
|
}
|
||||||
|
}, [mirrorStrategy]);
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
) => {
|
) => {
|
||||||
const { name, value } = e.target;
|
const { name, value, type } = e.target;
|
||||||
setConfig({
|
const checked = type === "checkbox" ? (e.target as HTMLInputElement).checked : undefined;
|
||||||
|
|
||||||
|
// Special handling for preserveOrgStructure changes
|
||||||
|
if (
|
||||||
|
name === "preserveOrgStructure" &&
|
||||||
|
config.preserveOrgStructure !== checked
|
||||||
|
) {
|
||||||
|
toast.info(
|
||||||
|
"Changing this setting may affect how repositories are accessed in Gitea. " +
|
||||||
|
"Existing mirrored repositories will still be accessible during sync operations.",
|
||||||
|
{
|
||||||
|
duration: 6000,
|
||||||
|
position: "top-center",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
...config,
|
...config,
|
||||||
[name]: value,
|
[name]: type === "checkbox" ? checked : value,
|
||||||
});
|
};
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
|
// Auto-save for all field changes
|
||||||
|
if (onAutoSave) {
|
||||||
|
onAutoSave(newConfig);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testConnection = async () => {
|
const testConnection = async () => {
|
||||||
@@ -63,7 +123,7 @@ export function GiteaConfigForm({ config, setConfig }: GiteaConfigFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full h-full flex flex-col">
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
||||||
<CardTitle className="text-lg font-semibold">
|
<CardTitle className="text-lg font-semibold">
|
||||||
Gitea Configuration
|
Gitea Configuration
|
||||||
@@ -78,7 +138,7 @@ export function GiteaConfigForm({ config, setConfig }: GiteaConfigFormProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-y-6">
|
<CardContent className="flex flex-col gap-y-6 flex-1">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="gitea-username"
|
htmlFor="gitea-username"
|
||||||
@@ -140,89 +200,47 @@ export function GiteaConfigForm({ config, setConfig }: GiteaConfigFormProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Separator />
|
||||||
<label
|
|
||||||
htmlFor="organization"
|
<OrganizationStrategy
|
||||||
className="block text-sm font-medium mb-1.5"
|
strategy={mirrorStrategy}
|
||||||
>
|
destinationOrg={config.organization}
|
||||||
Default Organization (Optional)
|
starredReposOrg={config.starredReposOrg}
|
||||||
</label>
|
onStrategyChange={setMirrorStrategy}
|
||||||
<input
|
githubUsername={githubUsername}
|
||||||
id="organization"
|
giteaUsername={config.username}
|
||||||
name="organization"
|
/>
|
||||||
type="text"
|
|
||||||
value={config.organization}
|
<Separator />
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
<OrganizationConfiguration
|
||||||
placeholder="Organization name"
|
strategy={mirrorStrategy}
|
||||||
/>
|
destinationOrg={config.organization}
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
starredReposOrg={config.starredReposOrg}
|
||||||
If specified, repositories will be mirrored to this organization.
|
personalReposOrg={config.personalReposOrg}
|
||||||
</p>
|
visibility={config.visibility}
|
||||||
</div>
|
onDestinationOrgChange={(org) => {
|
||||||
|
const newConfig = { ...config, organization: org };
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
setConfig(newConfig);
|
||||||
<div>
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
<label
|
}}
|
||||||
htmlFor="visibility"
|
onStarredReposOrgChange={(org) => {
|
||||||
className="block text-sm font-medium mb-1.5"
|
const newConfig = { ...config, starredReposOrg: org };
|
||||||
>
|
setConfig(newConfig);
|
||||||
Organization Visibility
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
</label>
|
}}
|
||||||
<Select
|
onPersonalReposOrgChange={(org) => {
|
||||||
name="visibility"
|
const newConfig = { ...config, personalReposOrg: org };
|
||||||
value={config.visibility}
|
setConfig(newConfig);
|
||||||
onValueChange={(value) =>
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
handleChange({
|
}}
|
||||||
target: { name: "visibility", value },
|
onVisibilityChange={(visibility) => {
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
const newConfig = { ...config, visibility };
|
||||||
}
|
setConfig(newConfig);
|
||||||
>
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
}}
|
||||||
<SelectValue placeholder="Select visibility" />
|
/>
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
|
||||||
{(["public", "private", "limited"] as GiteaOrgVisibility[]).map(
|
|
||||||
(option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={option}
|
|
||||||
value={option}
|
|
||||||
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
{option.charAt(0).toUpperCase() + option.slice(1)}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="starred-repos-org"
|
|
||||||
className="block text-sm font-medium mb-1.5"
|
|
||||||
>
|
|
||||||
Starred Repositories Organization
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="starred-repos-org"
|
|
||||||
name="starredReposOrg"
|
|
||||||
type="text"
|
|
||||||
value={config.starredReposOrg}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
||||||
placeholder="github"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Organization for starred repositories (default: github)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="">
|
|
||||||
{/* Footer content can be added here if needed */}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
226
src/components/config/MirrorOptionsForm.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Checkbox } from "../ui/checkbox";
|
||||||
|
import type { MirrorOptions } from "@/types/config";
|
||||||
|
import { RefreshCw, Info } from "lucide-react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
|
||||||
|
interface MirrorOptionsFormProps {
|
||||||
|
config: MirrorOptions;
|
||||||
|
setConfig: React.Dispatch<React.SetStateAction<MirrorOptions>>;
|
||||||
|
onAutoSave?: (config: MirrorOptions) => Promise<void>;
|
||||||
|
isAutoSaving?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MirrorOptionsForm({
|
||||||
|
config,
|
||||||
|
setConfig,
|
||||||
|
onAutoSave,
|
||||||
|
isAutoSaving = false,
|
||||||
|
}: MirrorOptionsFormProps) {
|
||||||
|
const handleChange = (name: string, checked: boolean) => {
|
||||||
|
let newConfig = { ...config };
|
||||||
|
|
||||||
|
if (name === "mirrorMetadata") {
|
||||||
|
newConfig.mirrorMetadata = checked;
|
||||||
|
// If disabling metadata, also disable all components
|
||||||
|
if (!checked) {
|
||||||
|
newConfig.metadataComponents = {
|
||||||
|
issues: false,
|
||||||
|
pullRequests: false,
|
||||||
|
labels: false,
|
||||||
|
milestones: false,
|
||||||
|
wiki: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (name.startsWith("metadataComponents.")) {
|
||||||
|
const componentName = name.split(".")[1] as keyof typeof config.metadataComponents;
|
||||||
|
newConfig.metadataComponents = {
|
||||||
|
...config.metadataComponents,
|
||||||
|
[componentName]: checked,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newConfig = {
|
||||||
|
...config,
|
||||||
|
[name]: checked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
|
// Auto-save
|
||||||
|
if (onAutoSave) {
|
||||||
|
onAutoSave(newConfig);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="self-start">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg font-semibold flex items-center justify-between">
|
||||||
|
Mirror Options
|
||||||
|
{isAutoSaving && (
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
|
||||||
|
<span className="text-xs">Auto-saving...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Repository Content */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-medium text-foreground">Repository Content</h4>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="mirror-releases"
|
||||||
|
checked={config.mirrorReleases}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("mirrorReleases", Boolean(checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="mirror-releases"
|
||||||
|
className="ml-2 text-sm select-none flex items-center"
|
||||||
|
>
|
||||||
|
Mirror releases
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="ml-1 cursor-pointer text-muted-foreground">
|
||||||
|
<Info size={14} />
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" className="max-w-xs text-xs">
|
||||||
|
Include GitHub releases and tags in the mirror
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="mirror-metadata"
|
||||||
|
checked={config.mirrorMetadata}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("mirrorMetadata", Boolean(checked))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="mirror-metadata"
|
||||||
|
className="ml-2 text-sm select-none flex items-center"
|
||||||
|
>
|
||||||
|
Mirror metadata
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="ml-1 cursor-pointer text-muted-foreground">
|
||||||
|
<Info size={14} />
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right" className="max-w-xs text-xs">
|
||||||
|
Include issues, pull requests, labels, milestones, and wiki
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata Components */}
|
||||||
|
{config.mirrorMetadata && (
|
||||||
|
<div className="ml-6 space-y-3 border-l-2 border-muted pl-4">
|
||||||
|
<h5 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
|
Metadata Components
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-issues"
|
||||||
|
checked={config.metadataComponents.issues}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("metadataComponents.issues", Boolean(checked))
|
||||||
|
}
|
||||||
|
disabled={!config.mirrorMetadata}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="metadata-issues"
|
||||||
|
className="ml-2 text-sm select-none"
|
||||||
|
>
|
||||||
|
Issues
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-pullRequests"
|
||||||
|
checked={config.metadataComponents.pullRequests}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("metadataComponents.pullRequests", Boolean(checked))
|
||||||
|
}
|
||||||
|
disabled={!config.mirrorMetadata}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="metadata-pullRequests"
|
||||||
|
className="ml-2 text-sm select-none"
|
||||||
|
>
|
||||||
|
Pull requests
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-labels"
|
||||||
|
checked={config.metadataComponents.labels}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("metadataComponents.labels", Boolean(checked))
|
||||||
|
}
|
||||||
|
disabled={!config.mirrorMetadata}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="metadata-labels"
|
||||||
|
className="ml-2 text-sm select-none"
|
||||||
|
>
|
||||||
|
Labels
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-milestones"
|
||||||
|
checked={config.metadataComponents.milestones}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("metadataComponents.milestones", Boolean(checked))
|
||||||
|
}
|
||||||
|
disabled={!config.mirrorMetadata}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="metadata-milestones"
|
||||||
|
className="ml-2 text-sm select-none"
|
||||||
|
>
|
||||||
|
Milestones
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="metadata-wiki"
|
||||||
|
checked={config.metadataComponents.wiki}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleChange("metadataComponents.wiki", Boolean(checked))
|
||||||
|
}
|
||||||
|
disabled={!config.mirrorMetadata}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="metadata-wiki"
|
||||||
|
className="ml-2 text-sm select-none"
|
||||||
|
>
|
||||||
|
Wiki
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
201
src/components/config/OrganizationConfiguration.tsx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Star, Globe, Lock, Shield, Info, MonitorCog } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { MirrorStrategy, GiteaOrgVisibility } from "@/types/config";
|
||||||
|
|
||||||
|
interface OrganizationConfigurationProps {
|
||||||
|
strategy: MirrorStrategy;
|
||||||
|
destinationOrg?: string;
|
||||||
|
starredReposOrg?: string;
|
||||||
|
personalReposOrg?: string;
|
||||||
|
visibility: GiteaOrgVisibility;
|
||||||
|
onDestinationOrgChange: (org: string) => void;
|
||||||
|
onStarredReposOrgChange: (org: string) => void;
|
||||||
|
onPersonalReposOrgChange: (org: string) => void;
|
||||||
|
onVisibilityChange: (visibility: GiteaOrgVisibility) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibilityOptions = [
|
||||||
|
{ value: "public" as GiteaOrgVisibility, label: "Public", icon: Globe, description: "Visible to everyone" },
|
||||||
|
{ value: "private" as GiteaOrgVisibility, label: "Private", icon: Lock, description: "Visible to members only" },
|
||||||
|
{ value: "limited" as GiteaOrgVisibility, label: "Limited", icon: Shield, description: "Visible to logged-in users" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps> = ({
|
||||||
|
strategy,
|
||||||
|
destinationOrg,
|
||||||
|
starredReposOrg,
|
||||||
|
personalReposOrg,
|
||||||
|
visibility,
|
||||||
|
onDestinationOrgChange,
|
||||||
|
onStarredReposOrgChange,
|
||||||
|
onPersonalReposOrgChange,
|
||||||
|
onVisibilityChange,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
|
<MonitorCog className="h-4 w-4" />
|
||||||
|
Organization Configuration
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* First row - Organization inputs with consistent layout */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{/* Left column - always shows starred repos org */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="starredReposOrg" className="text-sm font-normal flex items-center gap-2">
|
||||||
|
<Star className="h-3.5 w-3.5" />
|
||||||
|
Starred Repos Organization
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Starred repositories will be organized separately in this organization</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="starredReposOrg"
|
||||||
|
value={starredReposOrg || ""}
|
||||||
|
onChange={(e) => onStarredReposOrgChange(e.target.value)}
|
||||||
|
placeholder="starred"
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Keep starred repos organized separately
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column - shows destination org for single-org/mixed, personal repos org for preserve, empty div for others */}
|
||||||
|
{strategy === "single-org" || strategy === "mixed" ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="destinationOrg" className="text-sm font-normal flex items-center gap-2">
|
||||||
|
{strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{strategy === "mixed"
|
||||||
|
? "Personal repositories will be mirrored to this organization, while organization repos preserve their structure"
|
||||||
|
: "All repositories will be mirrored to this organization"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="destinationOrg"
|
||||||
|
value={destinationOrg || ""}
|
||||||
|
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
||||||
|
placeholder="github-mirrors"
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
{strategy === "mixed"
|
||||||
|
? "All personal repos will go to this organization"
|
||||||
|
: "Organization for consolidated repositories"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : strategy === "preserve" ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="personalReposOrg" className="text-sm font-normal flex items-center gap-2">
|
||||||
|
Personal Repos Organization
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Override where your personal repositories are mirrored (leave empty to use your username)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="personalReposOrg"
|
||||||
|
value={personalReposOrg || ""}
|
||||||
|
onChange={(e) => onPersonalReposOrgChange(e.target.value)}
|
||||||
|
placeholder="my-personal-mirrors"
|
||||||
|
className=""
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Override destination for your personal repos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="hidden md:block" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Second row - Organization Visibility (always shown) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-normal flex items-center gap-2">
|
||||||
|
Organization Visibility
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Default visibility for newly created organizations</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{visibilityOptions.map((option) => {
|
||||||
|
const Icon = option.icon;
|
||||||
|
const isSelected = visibility === option.value;
|
||||||
|
return (
|
||||||
|
<TooltipProvider key={option.value}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onVisibilityChange(option.value)}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-between px-3 py-2 rounded-md text-sm transition-all",
|
||||||
|
"border group",
|
||||||
|
isSelected
|
||||||
|
? "bg-accent border-accent-foreground/20"
|
||||||
|
: "bg-background hover:bg-accent/50 border-input"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon className="h-3.5 w-3.5" />
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</div>
|
||||||
|
<Info className="h-3 w-3 text-muted-foreground opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs">{option.description}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
434
src/components/config/OrganizationStrategy.tsx
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Info, GitBranch, FolderTree, Star, Building2, User, Building } from "lucide-react";
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from "@/components/ui/hover-card";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
|
||||||
|
interface OrganizationStrategyProps {
|
||||||
|
strategy: MirrorStrategy;
|
||||||
|
destinationOrg?: string;
|
||||||
|
starredReposOrg?: string;
|
||||||
|
onStrategyChange: (strategy: MirrorStrategy) => void;
|
||||||
|
githubUsername?: string;
|
||||||
|
giteaUsername?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategyConfig = {
|
||||||
|
preserve: {
|
||||||
|
title: "Preserve Structure",
|
||||||
|
icon: FolderTree,
|
||||||
|
description: "Keep the exact same organization structure as GitHub",
|
||||||
|
color: "text-blue-600 dark:text-blue-400",
|
||||||
|
bgColor: "bg-blue-50 dark:bg-blue-950/20",
|
||||||
|
borderColor: "border-blue-200 dark:border-blue-900",
|
||||||
|
repoColors: {
|
||||||
|
bg: "bg-blue-50 dark:bg-blue-950/30",
|
||||||
|
icon: "text-blue-600 dark:text-blue-400"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"single-org": {
|
||||||
|
title: "Single Organization",
|
||||||
|
icon: Building2,
|
||||||
|
description: "Consolidate all repositories into one Gitea organization",
|
||||||
|
color: "text-purple-600 dark:text-purple-400",
|
||||||
|
bgColor: "bg-purple-50 dark:bg-purple-950/20",
|
||||||
|
borderColor: "border-purple-200 dark:border-purple-900",
|
||||||
|
repoColors: {
|
||||||
|
bg: "bg-purple-50 dark:bg-purple-950/30",
|
||||||
|
icon: "text-purple-600 dark:text-purple-400"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flat-user": {
|
||||||
|
title: "User Repositories",
|
||||||
|
icon: User,
|
||||||
|
description: "Place all repositories directly under your user account",
|
||||||
|
color: "text-green-600 dark:text-green-400",
|
||||||
|
bgColor: "bg-green-50 dark:bg-green-950/20",
|
||||||
|
borderColor: "border-green-200 dark:border-green-900",
|
||||||
|
repoColors: {
|
||||||
|
bg: "bg-green-50 dark:bg-green-950/30",
|
||||||
|
icon: "text-green-600 dark:text-green-400"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mixed": {
|
||||||
|
title: "Mixed Mode",
|
||||||
|
icon: GitBranch,
|
||||||
|
description: "user repos in single org, org repos preserve structure",
|
||||||
|
color: "text-orange-600 dark:text-orange-400",
|
||||||
|
bgColor: "bg-orange-50 dark:bg-orange-950/20",
|
||||||
|
borderColor: "border-orange-200 dark:border-orange-900",
|
||||||
|
repoColors: {
|
||||||
|
bg: "bg-orange-50 dark:bg-orange-950/30",
|
||||||
|
icon: "text-orange-600 dark:text-orange-400"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const MappingPreview: React.FC<{
|
||||||
|
strategy: MirrorStrategy;
|
||||||
|
config: typeof strategyConfig.preserve;
|
||||||
|
destinationOrg?: string;
|
||||||
|
starredReposOrg?: string;
|
||||||
|
githubUsername?: string;
|
||||||
|
giteaUsername?: string;
|
||||||
|
}> = ({ strategy, config, destinationOrg, starredReposOrg, githubUsername, giteaUsername }) => {
|
||||||
|
const displayGithubUsername = githubUsername || "<username>";
|
||||||
|
const displayGiteaUsername = giteaUsername || "<username>";
|
||||||
|
const isGithubPlaceholder = !githubUsername;
|
||||||
|
const isGiteaPlaceholder = !giteaUsername;
|
||||||
|
|
||||||
|
if (strategy === "preserve") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
<span>awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<User className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span className={cn(isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strategy === "single-org") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
<span>awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{destinationOrg || "github-mirrors"}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{destinationOrg || "github-mirrors"}/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strategy === "flat-user") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
<span>awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<User className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span className={cn(isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<User className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span className={cn(isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strategy === "mixed") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
<span>awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{destinationOrg || "github-mirrors"}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||||
|
strategy,
|
||||||
|
destinationOrg,
|
||||||
|
starredReposOrg,
|
||||||
|
onStrategyChange,
|
||||||
|
githubUsername,
|
||||||
|
giteaUsername,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
|
<Building className="h-4 w-4" />
|
||||||
|
Organization Strategy
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Choose how your repositories will be organized in Gitea
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<HoverCard openDelay={200}>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Info className="h-3.5 w-3.5" />
|
||||||
|
<span>Override Options</span>
|
||||||
|
</button>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent side="left" align="start" className="w-[380px]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-sm mb-1.5">Fine-tune Your Mirror Destinations</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
After selecting a strategy, you can customize destinations for specific organizations and repositories.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2.5 pt-2 border-t">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building2 className="h-3.5 w-3.5 text-primary" />
|
||||||
|
<span className="text-xs font-medium">Organization Overrides</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground pl-5">
|
||||||
|
Click the edit button on any organization card to redirect all its repositories to a different Gitea organization.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GitBranch className="h-3.5 w-3.5 text-primary" />
|
||||||
|
<span className="text-xs font-medium">Repository Overrides</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground pl-5">
|
||||||
|
Use the inline editor in the repository table's "Destination" column to set custom destinations for individual repositories.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Star className="h-3.5 w-3.5 text-yellow-500" />
|
||||||
|
<span className="text-xs font-medium">Starred Repositories</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground pl-5">
|
||||||
|
Always go to the configured starred repos organization and cannot be overridden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
<span className="font-medium">Priority:</span> Repository override → Organization override → Strategy default
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RadioGroup value={strategy} onValueChange={onStrategyChange}>
|
||||||
|
<div className="grid grid-cols-1 2xl:grid-cols-2 gap-4">
|
||||||
|
{(Object.entries(strategyConfig) as [MirrorStrategy, typeof strategyConfig.preserve][]).map(([key, config]) => {
|
||||||
|
const isSelected = strategy === key;
|
||||||
|
const Icon = config.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
<label htmlFor={key} className="cursor-pointer">
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"relative",
|
||||||
|
isSelected && `${config.borderColor} border-2`,
|
||||||
|
!isSelected && "border-muted"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RadioGroupItem
|
||||||
|
value={key}
|
||||||
|
id={key}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"rounded-lg p-2",
|
||||||
|
isSelected ? config.bgColor : "bg-muted dark:bg-muted/50"
|
||||||
|
)}>
|
||||||
|
<Icon className={cn(
|
||||||
|
"h-4 w-4",
|
||||||
|
isSelected ? config.color : "text-muted-foreground dark:text-muted-foreground/70"
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h4 className="font-medium text-sm">{config.title}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{config.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HoverCard openDelay={200}>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<span
|
||||||
|
className="inline-flex p-1.5 hover:bg-muted rounded-md transition-colors cursor-help"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Info className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</span>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent side="left" align="center" className="w-[500px]">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="font-medium text-sm">Repository Mapping Preview</h4>
|
||||||
|
<MappingPreview
|
||||||
|
strategy={key}
|
||||||
|
config={config}
|
||||||
|
destinationOrg={destinationOrg}
|
||||||
|
starredReposOrg={starredReposOrg}
|
||||||
|
githubUsername={githubUsername}
|
||||||
|
giteaUsername={giteaUsername}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
47
src/components/config/ScheduleAndCleanupForm.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
||||||
|
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
||||||
|
import { Separator } from '../ui/separator';
|
||||||
|
import type { ScheduleConfig, DatabaseCleanupConfig } from '@/types/config';
|
||||||
|
|
||||||
|
interface ScheduleAndCleanupFormProps {
|
||||||
|
scheduleConfig: ScheduleConfig;
|
||||||
|
cleanupConfig: DatabaseCleanupConfig;
|
||||||
|
setScheduleConfig: (update: ScheduleConfig | ((prev: ScheduleConfig) => ScheduleConfig)) => void;
|
||||||
|
setCleanupConfig: (update: DatabaseCleanupConfig | ((prev: DatabaseCleanupConfig) => DatabaseCleanupConfig)) => void;
|
||||||
|
onAutoSaveSchedule?: (config: ScheduleConfig) => Promise<void>;
|
||||||
|
onAutoSaveCleanup?: (config: DatabaseCleanupConfig) => Promise<void>;
|
||||||
|
isAutoSavingSchedule?: boolean;
|
||||||
|
isAutoSavingCleanup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScheduleAndCleanupForm({
|
||||||
|
scheduleConfig,
|
||||||
|
cleanupConfig,
|
||||||
|
setScheduleConfig,
|
||||||
|
setCleanupConfig,
|
||||||
|
onAutoSaveSchedule,
|
||||||
|
onAutoSaveCleanup,
|
||||||
|
isAutoSavingSchedule,
|
||||||
|
isAutoSavingCleanup,
|
||||||
|
}: ScheduleAndCleanupFormProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<ScheduleConfigForm
|
||||||
|
config={scheduleConfig}
|
||||||
|
setConfig={setScheduleConfig}
|
||||||
|
onAutoSave={onAutoSaveSchedule}
|
||||||
|
isAutoSaving={isAutoSavingSchedule}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<DatabaseCleanupConfigForm
|
||||||
|
config={cleanupConfig}
|
||||||
|
setConfig={setCleanupConfig}
|
||||||
|
onAutoSave={onAutoSaveCleanup}
|
||||||
|
isAutoSaving={isAutoSavingCleanup}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ export function ScheduleConfigForm({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="self-start">
|
||||||
<CardContent className="pt-6 relative">
|
<CardContent className="pt-6 relative">
|
||||||
{isAutoSaving && (
|
{isAutoSaving && (
|
||||||
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
|
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ export function RepositoryList({ repositories }: RepositoryListProps) {
|
|||||||
Private
|
Private
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{repo.isForked && (
|
||||||
|
<span className="rounded-full bg-muted px-2 py-0.5 text-xs">
|
||||||
|
Fork
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { SiGitea } from "react-icons/si";
|
|
||||||
import { ModeToggle } from "@/components/theme/ModeToggle";
|
import { ModeToggle } from "@/components/theme/ModeToggle";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -64,7 +64,16 @@ export function Header({ currentPage, onNavigate }: HeaderProps) {
|
|||||||
}}
|
}}
|
||||||
className="flex items-center gap-2 py-1 hover:opacity-80 transition-opacity"
|
className="flex items-center gap-2 py-1 hover:opacity-80 transition-opacity"
|
||||||
>
|
>
|
||||||
<SiGitea className="h-6 w-6" />
|
<img
|
||||||
|
src="/logo-light.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-6 w-6 dark:hidden"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="/logo-dark.svg"
|
||||||
|
alt="Gitea Mirror Logo"
|
||||||
|
className="h-6 w-6 hidden dark:block"
|
||||||
|
/>
|
||||||
<span className="text-xl font-bold">Gitea Mirror</span>
|
<span className="text-xl font-bold">Gitea Mirror</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function VersionInfo() {
|
|||||||
return (
|
return (
|
||||||
<div className="text-xs text-muted-foreground text-center pt-2 pb-3 border-t border-border mt-2">
|
<div className="text-xs text-muted-foreground text-center pt-2 pb-3 border-t border-border mt-2">
|
||||||
{versionInfo.updateAvailable ? (
|
{versionInfo.updateAvailable ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-1">
|
||||||
<span>v{versionInfo.current}</span>
|
<span>v{versionInfo.current}</span>
|
||||||
<span className="text-primary">v{versionInfo.latest} available</span>
|
<span className="text-primary">v{versionInfo.latest} available</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
193
src/components/organizations/MirrorDestinationEditor.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ArrowRight, Edit3, RotateCcw, CheckCircle2, Building2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface MirrorDestinationEditorProps {
|
||||||
|
organizationId: string;
|
||||||
|
organizationName: string;
|
||||||
|
currentDestination?: string;
|
||||||
|
onUpdate: (newDestination: string | null) => Promise<void>;
|
||||||
|
isUpdating?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MirrorDestinationEditor({
|
||||||
|
organizationId,
|
||||||
|
organizationName,
|
||||||
|
currentDestination,
|
||||||
|
onUpdate,
|
||||||
|
isUpdating = false,
|
||||||
|
className,
|
||||||
|
}: MirrorDestinationEditorProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [editValue, setEditValue] = useState(currentDestination || "");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const hasOverride = currentDestination && currentDestination !== organizationName;
|
||||||
|
const effectiveDestination = currentDestination || organizationName;
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const trimmedValue = editValue.trim();
|
||||||
|
const newDestination = trimmedValue === "" || trimmedValue === organizationName
|
||||||
|
? null
|
||||||
|
: trimmedValue;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await onUpdate(newDestination);
|
||||||
|
setIsOpen(false);
|
||||||
|
toast.success(
|
||||||
|
newDestination
|
||||||
|
? `Destination updated to: ${newDestination}`
|
||||||
|
: "Destination reset to default"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to update destination");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
setEditValue("");
|
||||||
|
await handleSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditValue(currentDestination || "");
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center gap-2", className)}>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
<span className="font-medium">{organizationName}</span>
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
<span className={cn(
|
||||||
|
"font-medium",
|
||||||
|
hasOverride && "text-orange-600 dark:text-orange-400"
|
||||||
|
)}>
|
||||||
|
{effectiveDestination}
|
||||||
|
</span>
|
||||||
|
{hasOverride && (
|
||||||
|
<Badge variant="outline" className="h-4 px-1 text-[10px] border-orange-600 text-orange-600 dark:border-orange-400 dark:text-orange-400">
|
||||||
|
custom
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0 opacity-60 hover:opacity-100"
|
||||||
|
title="Edit mirror destination"
|
||||||
|
disabled={isUpdating || isLoading}
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-80" align="end">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium text-sm mb-1">Mirror Destination</h4>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Customize where this organization's repositories are mirrored to in Gitea.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Visual Preview */}
|
||||||
|
<div className="rounded-md bg-muted/50 p-3 space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground">Preview</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>{organizationName}</span>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Building2 className="h-4 w-4 text-primary" />
|
||||||
|
<span className="font-medium text-primary">
|
||||||
|
{editValue.trim() || organizationName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input Field */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="destination" className="text-xs">
|
||||||
|
Destination Organization
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="destination"
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
|
placeholder={organizationName}
|
||||||
|
className="h-8"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Leave empty to use the default GitHub organization name
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
{hasOverride && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full h-8 text-xs"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-3 w-3 mr-2" />
|
||||||
|
Reset to Default ({organizationName})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isLoading || (editValue.trim() === (currentDestination || ""))}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="h-3 w-3 mr-2 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Save"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, RefreshCw, FlipHorizontal, Plus } from "lucide-react";
|
import { Search, RefreshCw, FlipHorizontal } from "lucide-react";
|
||||||
import type { MirrorJob, Organization } from "@/lib/db/schema";
|
import type { MirrorJob, Organization } from "@/lib/db/schema";
|
||||||
import { OrganizationList } from "./OrganizationsList";
|
import { OrganizationList } from "./OrganizationsList";
|
||||||
import AddOrganizationDialog from "./AddOrganizationDialog";
|
import AddOrganizationDialog from "./AddOrganizationDialog";
|
||||||
@@ -26,6 +26,7 @@ import { useFilterParams } from "@/hooks/useFilterParams";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||||
import { useNavigation } from "@/components/layout/MainLayout";
|
import { useNavigation } from "@/components/layout/MainLayout";
|
||||||
|
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||||
|
|
||||||
export function Organization() {
|
export function Organization() {
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
@@ -34,6 +35,7 @@ export function Organization() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { isGitHubConfigured } = useConfigStatus();
|
const { isGitHubConfigured } = useConfigStatus();
|
||||||
const { navigationKey } = useNavigation();
|
const { navigationKey } = useNavigation();
|
||||||
|
const { registerRefreshCallback } = useLiveRefresh();
|
||||||
const { filter, setFilter } = useFilterParams({
|
const { filter, setFilter } = useFilterParams({
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
membershipRole: "",
|
membershipRole: "",
|
||||||
@@ -62,19 +64,23 @@ export function Organization() {
|
|||||||
onMessage: handleNewMessage,
|
onMessage: handleNewMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchOrganizations = useCallback(async () => {
|
const fetchOrganizations = useCallback(async (isLiveRefresh = false) => {
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't fetch organizations if GitHub is not configured
|
// Don't fetch organizations if GitHub is not configured
|
||||||
if (!isGitHubConfigured) {
|
if (!isGitHubConfigured) {
|
||||||
setIsLoading(false);
|
if (!isLiveRefresh) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
if (!isLiveRefresh) {
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await apiRequest<OrganizationsApiResponse>(
|
const response = await apiRequest<OrganizationsApiResponse>(
|
||||||
`/github/organizations?userId=${user.id}`,
|
`/github/organizations?userId=${user.id}`,
|
||||||
@@ -87,27 +93,47 @@ export function Organization() {
|
|||||||
setOrganizations(response.organizations);
|
setOrganizations(response.organizations);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error fetching organizations");
|
if (!isLiveRefresh) {
|
||||||
|
toast.error(response.error || "Error fetching organizations");
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
if (!isLiveRefresh) {
|
||||||
error instanceof Error ? error.message : "Error fetching organizations"
|
toast.error(
|
||||||
);
|
error instanceof Error ? error.message : "Error fetching organizations"
|
||||||
|
);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (!isLiveRefresh) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset loading state when component becomes active
|
// Reset loading state when component becomes active
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetchOrganizations();
|
fetchOrganizations(false); // Manual refresh, not live
|
||||||
}, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation
|
}, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
|
// Register with global live refresh system
|
||||||
|
useEffect(() => {
|
||||||
|
// Only register for live refresh if GitHub is configured
|
||||||
|
if (!isGitHubConfigured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unregister = registerRefreshCallback(() => {
|
||||||
|
fetchOrganizations(true); // Live refresh
|
||||||
|
});
|
||||||
|
|
||||||
|
return unregister;
|
||||||
|
}, [registerRefreshCallback, fetchOrganizations, isGitHubConfigured]);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
const success = await fetchOrganizations();
|
const success = await fetchOrganizations(false);
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Organizations refreshed successfully.");
|
toast.success("Organizations refreshed successfully.");
|
||||||
}
|
}
|
||||||
@@ -140,6 +166,12 @@ export function Organization() {
|
|||||||
return updated ? updated : org;
|
return updated ? updated : org;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Refresh organization data to get updated repository breakdown
|
||||||
|
// Use a small delay to allow the backend to process the mirroring request
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchOrganizations(true);
|
||||||
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error starting mirror job");
|
toast.error(response.error || "Error starting mirror job");
|
||||||
}
|
}
|
||||||
@@ -258,12 +290,7 @@ export function Organization() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get unique organization names for combobox (since Organization has no owner field)
|
|
||||||
const ownerOptions = Array.from(
|
|
||||||
new Set(
|
|
||||||
organizations.map((org) => org.name).filter((v): v is string => !!v)
|
|
||||||
)
|
|
||||||
).sort();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-8">
|
<div className="flex flex-col gap-y-8">
|
||||||
@@ -377,6 +404,7 @@ export function Organization() {
|
|||||||
loadingOrgIds={loadingOrgIds}
|
loadingOrgIds={loadingOrgIds}
|
||||||
onMirror={handleMirrorOrg}
|
onMirror={handleMirrorOrg}
|
||||||
onAddOrganization={() => setIsDialogOpen(true)}
|
onAddOrganization={() => setIsDialogOpen(true)}
|
||||||
|
onRefresh={() => fetchOrganizations(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddOrganizationDialog
|
<AddOrganizationDialog
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Plus, RefreshCw, Building2 } from "lucide-react";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { SiGithub } from "react-icons/si";
|
import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock } from "lucide-react";
|
||||||
|
import { SiGithub, SiGitea } from "react-icons/si";
|
||||||
import type { Organization } from "@/lib/db/schema";
|
import type { Organization } from "@/lib/db/schema";
|
||||||
import type { FilterParams } from "@/types/filter";
|
import type { FilterParams } from "@/types/filter";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { cn } from "@/lib/utils";
|
||||||
import { getStatusColor } from "@/lib/utils";
|
import { MirrorDestinationEditor } from "./MirrorDestinationEditor";
|
||||||
|
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
||||||
|
|
||||||
interface OrganizationListProps {
|
interface OrganizationListProps {
|
||||||
organizations: Organization[];
|
organizations: Organization[];
|
||||||
@@ -18,8 +20,25 @@ interface OrganizationListProps {
|
|||||||
onMirror: ({ orgId }: { orgId: string }) => Promise<void>;
|
onMirror: ({ orgId }: { orgId: string }) => Promise<void>;
|
||||||
loadingOrgIds: Set<string>;
|
loadingOrgIds: Set<string>;
|
||||||
onAddOrganization?: () => void;
|
onAddOrganization?: () => void;
|
||||||
|
onRefresh?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to get status badge variant and icon
|
||||||
|
const getStatusBadge = (status: string | null) => {
|
||||||
|
switch (status) {
|
||||||
|
case "imported":
|
||||||
|
return { variant: "secondary" as const, label: "Not Mirrored", icon: null };
|
||||||
|
case "mirroring":
|
||||||
|
return { variant: "outline" as const, label: "Mirroring", icon: Clock };
|
||||||
|
case "mirrored":
|
||||||
|
return { variant: "default" as const, label: "Mirrored", icon: Check };
|
||||||
|
case "failed":
|
||||||
|
return { variant: "destructive" as const, label: "Failed", icon: AlertCircle };
|
||||||
|
default:
|
||||||
|
return { variant: "secondary" as const, label: "Unknown", icon: null };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function OrganizationList({
|
export function OrganizationList({
|
||||||
organizations,
|
organizations,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -28,7 +47,59 @@ export function OrganizationList({
|
|||||||
onMirror,
|
onMirror,
|
||||||
loadingOrgIds,
|
loadingOrgIds,
|
||||||
onAddOrganization,
|
onAddOrganization,
|
||||||
|
onRefresh,
|
||||||
}: OrganizationListProps) {
|
}: OrganizationListProps) {
|
||||||
|
const { giteaConfig } = useGiteaConfig();
|
||||||
|
|
||||||
|
// Helper function to construct Gitea organization URL
|
||||||
|
const getGiteaOrgUrl = (organization: Organization): string | null => {
|
||||||
|
if (!giteaConfig?.url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only provide Gitea links for organizations that have been mirrored
|
||||||
|
const validStatuses = ['mirroring', 'mirrored'];
|
||||||
|
if (!validStatuses.includes(organization.status || '')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use destinationOrg if available, otherwise use the organization name
|
||||||
|
const orgName = organization.destinationOrg || organization.name;
|
||||||
|
if (!orgName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the base URL doesn't have a trailing slash
|
||||||
|
const baseUrl = giteaConfig.url.endsWith('/')
|
||||||
|
? giteaConfig.url.slice(0, -1)
|
||||||
|
: giteaConfig.url;
|
||||||
|
|
||||||
|
return `${baseUrl}/${orgName}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateDestination = async (orgId: string, newDestination: string | null) => {
|
||||||
|
// Call API to update organization destination
|
||||||
|
const response = await fetch(`/api/organizations/${orgId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
destinationOrg: newDestination,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || "Failed to update organization");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh organizations data
|
||||||
|
if (onRefresh) {
|
||||||
|
await onRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const hasAnyFilter = Object.values(filter).some(
|
const hasAnyFilter = Object.values(filter).some(
|
||||||
(val) => val?.toString().trim() !== ""
|
(val) => val?.toString().trim() !== ""
|
||||||
);
|
);
|
||||||
@@ -93,82 +164,206 @@ export function OrganizationList({
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{filteredOrganizations.map((org, index) => {
|
{filteredOrganizations.map((org, index) => {
|
||||||
const isLoading = loadingOrgIds.has(org.id ?? "");
|
const isLoading = loadingOrgIds.has(org.id ?? "");
|
||||||
|
const statusBadge = getStatusBadge(org.status);
|
||||||
|
const StatusIcon = statusBadge.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={index} className="overflow-hidden p-4">
|
<Card
|
||||||
<div className="flex items-center justify-between mb-2">
|
key={index}
|
||||||
<div className="flex items-center gap-2">
|
className={cn(
|
||||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
"overflow-hidden p-4 transition-all hover:shadow-md min-h-[160px]",
|
||||||
<a
|
isLoading && "opacity-75"
|
||||||
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
)}
|
||||||
className="font-medium hover:underline cursor-pointer"
|
>
|
||||||
>
|
<div className="flex items-start justify-between mb-3">
|
||||||
{org.name}
|
<div className="flex-1">
|
||||||
</a>
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<a
|
||||||
|
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
||||||
|
className="font-medium hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
{org.name}
|
||||||
|
</a>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-0.5 rounded-full capitalize ${
|
||||||
|
org.membershipRole === "member"
|
||||||
|
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||||
|
: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{org.membershipRole}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Destination override section */}
|
||||||
|
<div className="mt-2">
|
||||||
|
<MirrorDestinationEditor
|
||||||
|
organizationId={org.id!}
|
||||||
|
organizationName={org.name!}
|
||||||
|
currentDestination={org.destinationOrg}
|
||||||
|
onUpdate={(newDestination) => handleUpdateDestination(org.id!, newDestination)}
|
||||||
|
isUpdating={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<Badge variant={statusBadge.variant} className="ml-2">
|
||||||
className={`text-xs px-2 py-1 rounded-full capitalize ${
|
{StatusIcon && <StatusIcon className={cn(
|
||||||
org.membershipRole === "member"
|
"h-3 w-3",
|
||||||
? "bg-blue-100 text-blue-800"
|
org.status === "mirroring" && "animate-pulse"
|
||||||
: "bg-purple-100 text-purple-800"
|
)} />}
|
||||||
}`}
|
{statusBadge.label}
|
||||||
>
|
</Badge>
|
||||||
{org.membershipRole}
|
|
||||||
{/* needs to be updated */}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
{org.repositoryCount}{" "}
|
<div className="flex items-center justify-between">
|
||||||
{org.repositoryCount === 1 ? "repository" : "repositories"}
|
<span className="font-medium">
|
||||||
</p>
|
{org.repositoryCount}{" "}
|
||||||
|
{org.repositoryCount === 1 ? "repository" : "repositories"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/* Always render this section to prevent layout shift */}
|
||||||
|
<div className="flex gap-4 mt-2 text-xs min-h-[20px]">
|
||||||
|
{isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? (
|
||||||
|
<>
|
||||||
|
<Skeleton className="h-3 w-16" />
|
||||||
|
<Skeleton className="h-3 w-16" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{org.publicRepositoryCount !== undefined ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||||
|
{org.publicRepositoryCount} public
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-orange-500" />
|
||||||
|
{org.privateRepositoryCount} private
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||||
|
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{/* Show a placeholder if no counts are available to maintain height */}
|
||||||
|
{org.publicRepositoryCount === undefined &&
|
||||||
|
org.privateRepositoryCount === undefined &&
|
||||||
|
org.forkRepositoryCount === undefined && (
|
||||||
|
<span className="invisible">Loading counts...</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox
|
{org.status === "imported" && (
|
||||||
id={`include-${org.id}`}
|
<Button
|
||||||
name={`include-${org.id}`}
|
size="sm"
|
||||||
checked={org.status === "mirrored"}
|
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||||
disabled={
|
disabled={isLoading}
|
||||||
loadingOrgIds.has(org.id ?? "") ||
|
>
|
||||||
org.status === "mirrored" ||
|
{isLoading ? (
|
||||||
org.status === "mirroring"
|
<>
|
||||||
}
|
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||||
onCheckedChange={async (checked) => {
|
Starting...
|
||||||
if (checked && !org.isIncluded && org.id) {
|
</>
|
||||||
onMirror({ orgId: org.id });
|
) : (
|
||||||
}
|
"Mirror"
|
||||||
}}
|
)}
|
||||||
/>
|
</Button>
|
||||||
<label
|
)}
|
||||||
htmlFor={`include-${org.id}`}
|
|
||||||
className="ml-2 text-sm select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
{org.status === "mirroring" && (
|
||||||
>
|
<Button size="sm" disabled variant="outline">
|
||||||
Include in mirroring
|
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||||
</label>
|
Mirroring...
|
||||||
|
</Button>
|
||||||
{isLoading && (
|
)}
|
||||||
<RefreshCw className="opacity-50 h-4 w-4 animate-spin ml-4" />
|
|
||||||
|
{org.status === "mirrored" && (
|
||||||
|
<Button size="sm" disabled variant="secondary">
|
||||||
|
<Check className="h-3 w-3 mr-2" />
|
||||||
|
Mirrored
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{org.status === "failed" && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||||
|
Retrying...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<AlertCircle className="h-3 w-3 mr-2" />
|
||||||
|
Retry
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="ghost" size="icon" asChild>
|
<div className="flex items-center gap-1">
|
||||||
<a
|
{(() => {
|
||||||
href={`https://github.com/${org.name}`}
|
const giteaUrl = getGiteaOrgUrl(org);
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<SiGithub className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* dont know if this looks good. maybe revised */}
|
// Determine tooltip based on status and configuration
|
||||||
<div className="flex items-center gap-2 justify-end mt-4">
|
let tooltip: string;
|
||||||
<div
|
if (!giteaConfig?.url) {
|
||||||
className={`h-2 w-2 rounded-full ${getStatusColor(org.status)}`}
|
tooltip = "Gitea not configured";
|
||||||
/>
|
} else if (org.status === 'imported') {
|
||||||
<span className="text-sm capitalize">{org.status}</span>
|
tooltip = "Organization not yet mirrored to Gitea";
|
||||||
|
} else if (org.status === 'failed') {
|
||||||
|
tooltip = "Organization mirroring failed";
|
||||||
|
} else if (org.status === 'mirroring') {
|
||||||
|
tooltip = "Organization is being mirrored to Gitea";
|
||||||
|
} else if (giteaUrl) {
|
||||||
|
tooltip = "View on Gitea";
|
||||||
|
} else {
|
||||||
|
tooltip = "Gitea organization not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
return giteaUrl ? (
|
||||||
|
<Button variant="ghost" size="icon" asChild>
|
||||||
|
<a
|
||||||
|
href={giteaUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={tooltip}
|
||||||
|
>
|
||||||
|
<SiGitea className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="icon" disabled title={tooltip}>
|
||||||
|
<SiGitea className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<Button variant="ghost" size="icon" asChild>
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${org.name}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title="View on GitHub"
|
||||||
|
>
|
||||||
|
<SiGithub className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
187
src/components/repositories/InlineDestinationEditor.tsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { Edit3, Check, X } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { Repository } from "@/lib/db/schema";
|
||||||
|
|
||||||
|
interface InlineDestinationEditorProps {
|
||||||
|
repository: Repository;
|
||||||
|
giteaConfig: any;
|
||||||
|
onUpdate: (repoId: string, newDestination: string | null) => Promise<void>;
|
||||||
|
isUpdating?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InlineDestinationEditor({
|
||||||
|
repository,
|
||||||
|
giteaConfig,
|
||||||
|
onUpdate,
|
||||||
|
isUpdating = false,
|
||||||
|
className,
|
||||||
|
}: InlineDestinationEditorProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editValue, setEditValue] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Determine the default destination based on repository properties and config
|
||||||
|
const getDefaultDestination = () => {
|
||||||
|
// Starred repos always go to the configured starredReposOrg
|
||||||
|
if (repository.isStarred && giteaConfig?.starredReposOrg) {
|
||||||
|
return giteaConfig.starredReposOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mirror strategy
|
||||||
|
const strategy = giteaConfig?.mirrorStrategy || 'preserve';
|
||||||
|
|
||||||
|
if (strategy === 'single-org' && giteaConfig?.organization) {
|
||||||
|
// All repos go to a single organization
|
||||||
|
return giteaConfig.organization;
|
||||||
|
} else if (strategy === 'flat-user') {
|
||||||
|
// All repos go under the user account
|
||||||
|
return giteaConfig?.username || repository.owner;
|
||||||
|
} else {
|
||||||
|
// 'preserve' strategy or default
|
||||||
|
// For organization repos, use the organization name
|
||||||
|
if (repository.organization) {
|
||||||
|
return repository.organization;
|
||||||
|
}
|
||||||
|
// For personal repos, check if personalReposOrg is configured
|
||||||
|
if (!repository.organization && giteaConfig?.personalReposOrg) {
|
||||||
|
return giteaConfig.personalReposOrg;
|
||||||
|
}
|
||||||
|
// Default to the gitea username or owner
|
||||||
|
return giteaConfig?.username || repository.owner;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultDestination = getDefaultDestination();
|
||||||
|
const currentDestination = repository.destinationOrg || defaultDestination;
|
||||||
|
const hasOverride = repository.destinationOrg && repository.destinationOrg !== defaultDestination;
|
||||||
|
const isStarredRepo = repository.isStarred && giteaConfig?.starredReposOrg;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
inputRef.current.select();
|
||||||
|
}
|
||||||
|
}, [isEditing]);
|
||||||
|
|
||||||
|
const handleStartEdit = () => {
|
||||||
|
if (isStarredRepo) return; // Don't allow editing starred repos
|
||||||
|
setEditValue(currentDestination);
|
||||||
|
setIsEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const trimmedValue = editValue.trim();
|
||||||
|
const newDestination = trimmedValue === defaultDestination ? null : trimmedValue;
|
||||||
|
|
||||||
|
if (trimmedValue === currentDestination) {
|
||||||
|
setIsEditing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await onUpdate(repository.id!, newDestination);
|
||||||
|
setIsEditing(false);
|
||||||
|
} catch (error) {
|
||||||
|
// Revert on error
|
||||||
|
setEditValue(currentDestination);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditValue(currentDestination);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center gap-1", className)}>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e) => setEditValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleCancel}
|
||||||
|
className="h-6 text-sm px-2 py-0 w-24"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<Check className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col gap-0.5", className)}>
|
||||||
|
{/* Show GitHub org if exists */}
|
||||||
|
{repository.organization && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{repository.organization}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show Gitea destination */}
|
||||||
|
<div className="flex items-center gap-1 group">
|
||||||
|
<span className="text-sm">
|
||||||
|
{currentDestination || "-"}
|
||||||
|
</span>
|
||||||
|
{hasOverride && (
|
||||||
|
<Badge variant="outline" className="h-4 px-1 text-[10px] ml-1">
|
||||||
|
custom
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{isStarredRepo && (
|
||||||
|
<Badge variant="secondary" className="h-4 px-1 text-[10px] ml-1">
|
||||||
|
starred
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{!isStarredRepo && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-4 w-4 p-0 opacity-0 group-hover:opacity-60 hover:opacity-100 ml-1"
|
||||||
|
onClick={handleStartEdit}
|
||||||
|
disabled={isUpdating || isLoading}
|
||||||
|
title="Edit destination"
|
||||||
|
>
|
||||||
|
<Edit3 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "../ui/select";
|
} from "../ui/select";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, RefreshCw, FlipHorizontal } from "lucide-react";
|
import { Search, RefreshCw, FlipHorizontal, RotateCcw, X } from "lucide-react";
|
||||||
import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
|
import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
|
||||||
import { useSSE } from "@/hooks/useSEE";
|
import { useSSE } from "@/hooks/useSEE";
|
||||||
import { useFilterParams } from "@/hooks/useFilterParams";
|
import { useFilterParams } from "@/hooks/useFilterParams";
|
||||||
@@ -46,6 +46,7 @@ export default function Repository() {
|
|||||||
owner: "",
|
owner: "",
|
||||||
});
|
});
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||||
|
const [selectedRepoIds, setSelectedRepoIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Read organization filter from URL when component mounts
|
// Read organization filter from URL when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -254,6 +255,143 @@ export default function Repository() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bulk action handlers
|
||||||
|
const handleBulkMirror = async () => {
|
||||||
|
if (selectedRepoIds.size === 0) return;
|
||||||
|
|
||||||
|
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||||
|
const eligibleRepos = selectedRepos.filter(
|
||||||
|
repo => repo.status === "imported" || repo.status === "failed"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eligibleRepos.length === 0) {
|
||||||
|
toast.info("No eligible repositories to mirror in selection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoIds = eligibleRepos.map(repo => repo.id as string);
|
||||||
|
|
||||||
|
setLoadingRepoIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
repoIds.forEach(id => newSet.add(id));
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<MirrorRepoResponse>("/job/mirror-repo", {
|
||||||
|
method: "POST",
|
||||||
|
data: { userId: user?.id, repositoryIds: repoIds }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(`Mirroring started for ${repoIds.length} repositories`);
|
||||||
|
setRepositories(prevRepos =>
|
||||||
|
prevRepos.map(repo => {
|
||||||
|
const updated = response.repositories.find(r => r.id === repo.id);
|
||||||
|
return updated ? updated : repo;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setSelectedRepoIds(new Set());
|
||||||
|
} else {
|
||||||
|
showErrorToast(response.error || "Error starting mirror jobs", toast);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
} finally {
|
||||||
|
setLoadingRepoIds(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkSync = async () => {
|
||||||
|
if (selectedRepoIds.size === 0) return;
|
||||||
|
|
||||||
|
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||||
|
const eligibleRepos = selectedRepos.filter(
|
||||||
|
repo => repo.status === "mirrored" || repo.status === "synced"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eligibleRepos.length === 0) {
|
||||||
|
toast.info("No eligible repositories to sync in selection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoIds = eligibleRepos.map(repo => repo.id as string);
|
||||||
|
|
||||||
|
setLoadingRepoIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
repoIds.forEach(id => newSet.add(id));
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<SyncRepoResponse>("/job/sync-repo", {
|
||||||
|
method: "POST",
|
||||||
|
data: { userId: user?.id, repositoryIds: repoIds }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(`Syncing started for ${repoIds.length} repositories`);
|
||||||
|
setRepositories(prevRepos =>
|
||||||
|
prevRepos.map(repo => {
|
||||||
|
const updated = response.repositories.find(r => r.id === repo.id);
|
||||||
|
return updated ? updated : repo;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setSelectedRepoIds(new Set());
|
||||||
|
} else {
|
||||||
|
showErrorToast(response.error || "Error starting sync jobs", toast);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
} finally {
|
||||||
|
setLoadingRepoIds(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkRetry = async () => {
|
||||||
|
if (selectedRepoIds.size === 0) return;
|
||||||
|
|
||||||
|
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||||
|
const eligibleRepos = selectedRepos.filter(repo => repo.status === "failed");
|
||||||
|
|
||||||
|
if (eligibleRepos.length === 0) {
|
||||||
|
toast.info("No failed repositories in selection to retry");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoIds = eligibleRepos.map(repo => repo.id as string);
|
||||||
|
|
||||||
|
setLoadingRepoIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
repoIds.forEach(id => newSet.add(id));
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<RetryRepoResponse>("/job/retry-repo", {
|
||||||
|
method: "POST",
|
||||||
|
data: { userId: user?.id, repositoryIds: repoIds }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(`Retrying ${repoIds.length} repositories`);
|
||||||
|
setRepositories(prevRepos =>
|
||||||
|
prevRepos.map(repo => {
|
||||||
|
const updated = response.repositories.find(r => r.id === repo.id);
|
||||||
|
return updated ? updated : repo;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setSelectedRepoIds(new Set());
|
||||||
|
} else {
|
||||||
|
showErrorToast(response.error || "Error retrying jobs", toast);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error, toast);
|
||||||
|
} finally {
|
||||||
|
setLoadingRepoIds(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSyncRepo = async ({ repoId }: { repoId: string }) => {
|
const handleSyncRepo = async ({ repoId }: { repoId: string }) => {
|
||||||
try {
|
try {
|
||||||
if (!user || !user.id) {
|
if (!user || !user.id) {
|
||||||
@@ -392,6 +530,35 @@ export default function Repository() {
|
|||||||
)
|
)
|
||||||
).sort();
|
).sort();
|
||||||
|
|
||||||
|
// Determine what actions are available for selected repositories
|
||||||
|
const getAvailableActions = () => {
|
||||||
|
if (selectedRepoIds.size === 0) return [];
|
||||||
|
|
||||||
|
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||||
|
const statuses = new Set(selectedRepos.map(repo => repo.status));
|
||||||
|
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
// Check if any selected repos can be mirrored
|
||||||
|
if (selectedRepos.some(repo => repo.status === "imported" || repo.status === "failed")) {
|
||||||
|
actions.push('mirror');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any selected repos can be synced
|
||||||
|
if (selectedRepos.some(repo => repo.status === "mirrored" || repo.status === "synced")) {
|
||||||
|
actions.push('sync');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any selected repos are failed
|
||||||
|
if (selectedRepos.some(repo => repo.status === "failed")) {
|
||||||
|
actions.push('retry');
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableActions = getAvailableActions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-8">
|
<div className="flex flex-col gap-y-8">
|
||||||
{/* Combine search and actions into a single flex row */}
|
{/* Combine search and actions into a single flex row */}
|
||||||
@@ -459,14 +626,69 @@ export default function Repository() {
|
|||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
{/* Context-aware action buttons */}
|
||||||
variant="default"
|
{selectedRepoIds.size === 0 ? (
|
||||||
onClick={handleMirrorAllRepos}
|
<Button
|
||||||
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
variant="default"
|
||||||
>
|
onClick={handleMirrorAllRepos}
|
||||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
||||||
Mirror All
|
>
|
||||||
</Button>
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
|
Mirror All
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1 bg-muted/50 rounded-md">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedRepoIds.size} selected
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => setSelectedRepoIds(new Set())}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{availableActions.includes('mirror') && (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBulkMirror}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||||
|
Mirror ({selectedRepoIds.size})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('sync') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBulkSync}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Sync ({selectedRepoIds.size})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{availableActions.includes('retry') && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBulkRetry}
|
||||||
|
disabled={loadingRepoIds.size > 0}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4 mr-2" />
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isGitHubConfigured ? (
|
{!isGitHubConfigured ? (
|
||||||
@@ -497,6 +719,9 @@ export default function Repository() {
|
|||||||
onSync={handleSyncRepo}
|
onSync={handleSyncRepo}
|
||||||
onRetry={handleRetryRepoAction}
|
onRetry={handleRetryRepoAction}
|
||||||
loadingRepoIds={loadingRepoIds}
|
loadingRepoIds={loadingRepoIds}
|
||||||
|
selectedRepoIds={selectedRepoIds}
|
||||||
|
onSelectionChange={setSelectedRepoIds}
|
||||||
|
onRefresh={() => fetchRepositories(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw } from "lucide-react";
|
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star } from "lucide-react";
|
||||||
import { SiGithub, SiGitea } from "react-icons/si";
|
import { SiGithub, SiGitea } from "react-icons/si";
|
||||||
import type { Repository } from "@/lib/db/schema";
|
import type { Repository } from "@/lib/db/schema";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -9,6 +9,14 @@ import { formatDate, getStatusColor } from "@/lib/utils";
|
|||||||
import type { FilterParams } from "@/types/filter";
|
import type { FilterParams } from "@/types/filter";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { InlineDestinationEditor } from "./InlineDestinationEditor";
|
||||||
|
|
||||||
interface RepositoryTableProps {
|
interface RepositoryTableProps {
|
||||||
repositories: Repository[];
|
repositories: Repository[];
|
||||||
@@ -20,6 +28,9 @@ interface RepositoryTableProps {
|
|||||||
onSync: ({ repoId }: { repoId: string }) => Promise<void>;
|
onSync: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||||
onRetry: ({ repoId }: { repoId: string }) => Promise<void>;
|
onRetry: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||||
loadingRepoIds: Set<string>;
|
loadingRepoIds: Set<string>;
|
||||||
|
selectedRepoIds: Set<string>;
|
||||||
|
onSelectionChange: (selectedIds: Set<string>) => void;
|
||||||
|
onRefresh?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RepositoryTable({
|
export default function RepositoryTable({
|
||||||
@@ -32,10 +43,36 @@ export default function RepositoryTable({
|
|||||||
onSync,
|
onSync,
|
||||||
onRetry,
|
onRetry,
|
||||||
loadingRepoIds,
|
loadingRepoIds,
|
||||||
|
selectedRepoIds,
|
||||||
|
onSelectionChange,
|
||||||
|
onRefresh,
|
||||||
}: RepositoryTableProps) {
|
}: RepositoryTableProps) {
|
||||||
const tableParentRef = useRef<HTMLDivElement>(null);
|
const tableParentRef = useRef<HTMLDivElement>(null);
|
||||||
const { giteaConfig } = useGiteaConfig();
|
const { giteaConfig } = useGiteaConfig();
|
||||||
|
|
||||||
|
const handleUpdateDestination = async (repoId: string, newDestination: string | null) => {
|
||||||
|
// Call API to update repository destination
|
||||||
|
const response = await fetch(`/api/repositories/${repoId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
destinationOrg: newDestination,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || "Failed to update repository");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh repositories data
|
||||||
|
if (onRefresh) {
|
||||||
|
await onRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function to construct Gitea repository URL
|
// Helper function to construct Gitea repository URL
|
||||||
const getGiteaRepoUrl = (repository: Repository): string | null => {
|
const getGiteaRepoUrl = (repository: Repository): string | null => {
|
||||||
if (!giteaConfig?.url) {
|
if (!giteaConfig?.url) {
|
||||||
@@ -105,9 +142,36 @@ export default function RepositoryTable({
|
|||||||
overscan: 5,
|
overscan: 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Selection handlers
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
const allIds = new Set(filteredRepositories.map(repo => repo.id).filter((id): id is string => !!id));
|
||||||
|
onSelectionChange(allIds);
|
||||||
|
} else {
|
||||||
|
onSelectionChange(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectRepo = (repoId: string, checked: boolean) => {
|
||||||
|
const newSelection = new Set(selectedRepoIds);
|
||||||
|
if (checked) {
|
||||||
|
newSelection.add(repoId);
|
||||||
|
} else {
|
||||||
|
newSelection.delete(repoId);
|
||||||
|
}
|
||||||
|
onSelectionChange(newSelection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAllSelected = filteredRepositories.length > 0 &&
|
||||||
|
filteredRepositories.every(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||||
|
const isPartiallySelected = selectedRepoIds.size > 0 && !isAllSelected;
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<div className="border rounded-md">
|
<div className="border rounded-md">
|
||||||
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
||||||
|
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||||
|
<Skeleton className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
||||||
Repository
|
Repository
|
||||||
</div>
|
</div>
|
||||||
@@ -132,6 +196,9 @@ export default function RepositoryTable({
|
|||||||
key={i}
|
key={i}
|
||||||
className="h-[65px] flex items-center justify-between border-b bg-transparent"
|
className="h-[65px] flex items-center justify-between border-b bg-transparent"
|
||||||
>
|
>
|
||||||
|
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||||
|
<Skeleton className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
||||||
<Skeleton className="h-full w-full" />
|
<Skeleton className="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
@@ -187,6 +254,14 @@ export default function RepositoryTable({
|
|||||||
<div className="flex flex-col border rounded-md">
|
<div className="flex flex-col border rounded-md">
|
||||||
{/* table header */}
|
{/* table header */}
|
||||||
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
||||||
|
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||||
|
<Checkbox
|
||||||
|
checked={isAllSelected}
|
||||||
|
indeterminate={isPartiallySelected}
|
||||||
|
onCheckedChange={handleSelectAll}
|
||||||
|
aria-label="Select all repositories"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
||||||
Repository
|
Repository
|
||||||
</div>
|
</div>
|
||||||
@@ -235,11 +310,25 @@ export default function RepositoryTable({
|
|||||||
data-index={virtualRow.index}
|
data-index={virtualRow.index}
|
||||||
className="h-[65px] flex items-center justify-between bg-transparent border-b hover:bg-muted/50" //the height is set according to the row content. right now the highest row is in the repo column which is arround 64.99px
|
className="h-[65px] flex items-center justify-between bg-transparent border-b hover:bg-muted/50" //the height is set according to the row content. right now the highest row is in the repo column which is arround 64.99px
|
||||||
>
|
>
|
||||||
|
{/* Checkbox */}
|
||||||
|
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||||
|
<Checkbox
|
||||||
|
checked={repo.id ? selectedRepoIds.has(repo.id) : false}
|
||||||
|
onCheckedChange={(checked) => repo.id && handleSelectRepo(repo.id, !!checked)}
|
||||||
|
aria-label={`Select ${repo.name}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Repository */}
|
{/* Repository */}
|
||||||
<div className="h-full p-3 flex items-center gap-2 flex-[2.5]">
|
<div className="h-full p-3 flex items-center gap-2 flex-[2.5]">
|
||||||
<GitFork className="h-4 w-4 text-muted-foreground" />
|
<GitFork className="h-4 w-4 text-muted-foreground" />
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<div className="font-medium">{repo.name}</div>
|
<div className="font-medium flex items-center gap-1">
|
||||||
|
{repo.name}
|
||||||
|
{repo.isStarred && (
|
||||||
|
<Star className="h-3 w-3 fill-yellow-500 text-yellow-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{repo.fullName}
|
{repo.fullName}
|
||||||
</div>
|
</div>
|
||||||
@@ -249,6 +338,11 @@ export default function RepositoryTable({
|
|||||||
Private
|
Private
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{repo.isForked && (
|
||||||
|
<span className="ml-2 rounded-full bg-muted px-2 py-0.5 text-xs">
|
||||||
|
Fork
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Owner */}
|
{/* Owner */}
|
||||||
@@ -258,7 +352,12 @@ export default function RepositoryTable({
|
|||||||
|
|
||||||
{/* Organization */}
|
{/* Organization */}
|
||||||
<div className="h-full p-3 flex items-center flex-[1]">
|
<div className="h-full p-3 flex items-center flex-[1]">
|
||||||
<p className="text-sm"> {repo.organization || "-"}</p>
|
<InlineDestinationEditor
|
||||||
|
repository={repo}
|
||||||
|
giteaConfig={giteaConfig}
|
||||||
|
onUpdate={handleUpdateDestination}
|
||||||
|
isUpdating={loadingRepoIds.has(repo.id ?? "")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Last Mirrored */}
|
{/* Last Mirrored */}
|
||||||
@@ -272,12 +371,26 @@ export default function RepositoryTable({
|
|||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div className="h-full p-3 flex items-center gap-x-2 flex-[1]">
|
<div className="h-full p-3 flex items-center gap-x-2 flex-[1]">
|
||||||
<div
|
{repo.status === "failed" && repo.errorMessage ? (
|
||||||
className={`h-2 w-2 rounded-full ${getStatusColor(
|
<TooltipProvider>
|
||||||
repo.status
|
<Tooltip>
|
||||||
)}`}
|
<TooltipTrigger asChild>
|
||||||
/>
|
<div className="flex items-center gap-x-2 cursor-help">
|
||||||
<span className="text-sm capitalize">{repo.status}</span>
|
<div className={`h-2 w-2 rounded-full ${getStatusColor(repo.status)}`} />
|
||||||
|
<span className="text-sm capitalize underline decoration-dotted">{repo.status}</span>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
<p className="text-sm">{repo.errorMessage}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className={`h-2 w-2 rounded-full ${getStatusColor(repo.status)}`} />
|
||||||
|
<span className="text-sm capitalize">{repo.status}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|||||||
64
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||||
|
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
data-slot="accordion-item"
|
||||||
|
className={cn("border-b last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
data-slot="accordion-content"
|
||||||
|
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
46
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
31
src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||||
|
|
||||||
|
function Collapsible({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
|
||||||
|
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleTrigger
|
||||||
|
data-slot="collapsible-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CollapsibleContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
|
||||||
|
return (
|
||||||
|
<CollapsiblePrimitive.CollapsibleContent
|
||||||
|
data-slot="collapsible-content"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||||
42
src/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function HoverCard({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||||
|
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function HoverCardTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function HoverCardContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||||
|
<HoverCardPrimitive.Content
|
||||||
|
data-slot="hover-card-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</HoverCardPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||||
43
src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
|
import { CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
className={cn(
|
||||||
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
||||||
56
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
||||||
26
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
@@ -19,7 +19,11 @@ function TooltipProvider({
|
|||||||
function Tooltip({
|
function Tooltip({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TooltipTrigger({
|
function TooltipTrigger({
|
||||||
@@ -40,7 +44,7 @@ function TooltipContent({
|
|||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-[var(--radix-tooltip-content-transform-origin)] rounded-md px-3 py-1.5 text-xs text-balance",
|
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,17 +1,4 @@
|
|||||||
import { defineCollection, z } from 'astro:content';
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
// Define a schema for the documentation collection
|
// Export empty collections since docs have been moved
|
||||||
const docsCollection = defineCollection({
|
export const collections = {};
|
||||||
type: 'content',
|
|
||||||
schema: z.object({
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
order: z.number().optional(),
|
|
||||||
updatedDate: z.date().optional(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export the collections
|
|
||||||
export const collections = {
|
|
||||||
'docs': docsCollection,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Architecture"
|
|
||||||
description: "Comprehensive overview of the Gitea Mirror application architecture."
|
|
||||||
order: 1
|
|
||||||
updatedDate: 2025-05-22
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-2xl font-bold text-foreground">Gitea Mirror Architecture</h1>
|
|
||||||
<p class="text-muted-foreground mt-2">This document provides a comprehensive overview of the Gitea Mirror application architecture, including component diagrams, project structure, and detailed explanations of each part of the system.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## System Overview
|
|
||||||
|
|
||||||
<div class="mb-4">
|
|
||||||
<p class="text-muted-foreground">Gitea Mirror is a web application that automates the mirroring of GitHub repositories to Gitea instances. It provides a user-friendly interface for configuring, monitoring, and managing mirroring operations without requiring users to edit configuration files or run Docker commands.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
The application is built using:
|
|
||||||
|
|
||||||
- <span class="font-semibold text-foreground">Astro</span>: Web framework for the frontend
|
|
||||||
- <span class="font-semibold text-foreground">React</span>: Component library for interactive UI elements
|
|
||||||
- <span class="font-semibold text-foreground">Shadcn UI</span>: UI component library built on Tailwind CSS
|
|
||||||
- <span class="font-semibold text-foreground">SQLite</span>: Database for storing configuration, state, and events
|
|
||||||
- <span class="font-semibold text-foreground">Bun</span>: Runtime environment for the backend
|
|
||||||
- <span class="font-semibold text-foreground">Drizzle ORM</span>: Type-safe ORM for database interactions
|
|
||||||
|
|
||||||
## Architecture Diagram
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
subgraph "Gitea Mirror"
|
|
||||||
Frontend["Frontend<br/>(Astro + React)"]
|
|
||||||
Backend["Backend<br/>(Bun)"]
|
|
||||||
Database["Database<br/>(SQLite + Drizzle)"]
|
|
||||||
|
|
||||||
Frontend <--> Backend
|
|
||||||
Backend <--> Database
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "External APIs"
|
|
||||||
GitHub["GitHub API"]
|
|
||||||
Gitea["Gitea API"]
|
|
||||||
end
|
|
||||||
|
|
||||||
Backend --> GitHub
|
|
||||||
Backend --> Gitea
|
|
||||||
```
|
|
||||||
|
|
||||||
## Component Breakdown
|
|
||||||
|
|
||||||
### Frontend (Astro + React)
|
|
||||||
|
|
||||||
The frontend is built with Astro, a modern web framework that allows for server-side rendering and partial hydration. React components are used for interactive elements, providing a responsive and dynamic user interface.
|
|
||||||
|
|
||||||
Key frontend components:
|
|
||||||
|
|
||||||
- **Dashboard**: Overview of mirroring status and recent activity
|
|
||||||
- **Repository Management**: Interface for managing repositories to mirror
|
|
||||||
- **Organization Management**: Interface for managing GitHub organizations
|
|
||||||
- **Configuration**: Settings for GitHub and Gitea connections
|
|
||||||
- **Activity Log**: Detailed log of mirroring operations
|
|
||||||
|
|
||||||
### Backend (Bun)
|
|
||||||
|
|
||||||
The backend is built with Bun and provides API endpoints for the frontend to interact with. It handles:
|
|
||||||
|
|
||||||
- Authentication and user management
|
|
||||||
- GitHub API integration
|
|
||||||
- Gitea API integration
|
|
||||||
- Mirroring operations
|
|
||||||
- Database interactions
|
|
||||||
|
|
||||||
### Database (SQLite + Drizzle ORM)
|
|
||||||
|
|
||||||
SQLite with Bun's native SQLite driver is used for data persistence, with Drizzle ORM providing type-safe database interactions. The database stores:
|
|
||||||
|
|
||||||
- User accounts and authentication data
|
|
||||||
- GitHub and Gitea configuration
|
|
||||||
- Repository and organization information
|
|
||||||
- Mirroring job history and status
|
|
||||||
- Event notifications and their read status
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
1. **User Authentication**: Users authenticate through the frontend, which communicates with the backend to validate credentials.
|
|
||||||
2. **Configuration**: Users configure GitHub and Gitea settings through the UI, which are stored in the SQLite database.
|
|
||||||
3. **Repository Discovery**: The backend queries the GitHub API to discover repositories based on user configuration.
|
|
||||||
4. **Mirroring Process**: When triggered, the backend fetches repository data from GitHub and pushes it to Gitea.
|
|
||||||
5. **Status Tracking**: All operations are logged in the database and displayed in the Activity Log.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
gitea-mirror/
|
|
||||||
├── src/ # Source code
|
|
||||||
│ ├── components/ # React components
|
|
||||||
│ ├── content/ # Documentation and content
|
|
||||||
│ ├── layouts/ # Astro layout components
|
|
||||||
│ ├── lib/ # Utility functions and database
|
|
||||||
│ ├── pages/ # Astro pages and API routes
|
|
||||||
│ └── styles/ # CSS and Tailwind styles
|
|
||||||
├── public/ # Static assets
|
|
||||||
├── data/ # Database and persistent data
|
|
||||||
├── docker/ # Docker configuration
|
|
||||||
└── scripts/ # Utility scripts for deployment and maintenance
|
|
||||||
├── gitea-mirror-lxc-local.sh # Local LXC deployment script
|
|
||||||
└── manage-db.ts # Database management tool
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Options
|
|
||||||
|
|
||||||
Gitea Mirror supports multiple deployment options:
|
|
||||||
|
|
||||||
1. **Docker**: Run as a containerized application using Docker and docker-compose
|
|
||||||
2. **LXC Containers**: Deploy in Linux Containers (LXC) on Proxmox VE (using community script by [Tobias/CrazyWolf13](https://github.com/CrazyWolf13)) or local workstations
|
|
||||||
3. **Native**: Run directly on the host system using Bun runtime
|
|
||||||
|
|
||||||
Each deployment method has its own advantages:
|
|
||||||
|
|
||||||
- **Docker**: Isolation, easy updates, consistent environment
|
|
||||||
- **LXC**: Lightweight virtualization, better performance than Docker, system-level isolation
|
|
||||||
- **Native**: Best performance, direct access to system resources
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Configuration"
|
|
||||||
description: "Guide to configuring Gitea Mirror for your environment."
|
|
||||||
order: 2
|
|
||||||
updatedDate: 2025-05-22
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-2xl font-bold text-foreground">Gitea Mirror Configuration Guide</h1>
|
|
||||||
<p class="text-muted-foreground mt-2">This guide provides detailed information on how to configure Gitea Mirror for your environment.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Configuration Methods
|
|
||||||
|
|
||||||
Gitea Mirror can be configured using:
|
|
||||||
|
|
||||||
1. <span class="font-semibold text-foreground">Environment Variables</span>: Set configuration options through environment variables
|
|
||||||
2. <span class="font-semibold text-foreground">Web UI</span>: Configure the application through the web interface after installation
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
The following environment variables can be used to configure Gitea Mirror:
|
|
||||||
|
|
||||||
| Variable | Description | Default Value | Example |
|
|
||||||
|----------|-------------|---------------|---------|
|
|
||||||
| `NODE_ENV` | Runtime environment (development, production, test) | `development` | `production` |
|
|
||||||
| `DATABASE_URL` | SQLite database URL | `file:data/gitea-mirror.db` | `file:path/to/your/database.db` |
|
|
||||||
| `JWT_SECRET` | Secret key for JWT authentication | Auto-generated secure random string | `your-secure-random-string` |
|
|
||||||
| `HOST` | Server host | `localhost` | `0.0.0.0` |
|
|
||||||
| `PORT` | Server port | `4321` | `8080` |
|
|
||||||
|
|
||||||
### Important Security Note
|
|
||||||
|
|
||||||
The application will automatically generate a secure random `JWT_SECRET` on first run if one isn't provided or if the default value is used. This generated secret is stored in the data directory for persistence across container restarts.
|
|
||||||
|
|
||||||
While this auto-generation feature provides good security by default, you can still explicitly set your own `JWT_SECRET` for complete control over your deployment.
|
|
||||||
|
|
||||||
## Web UI Configuration
|
|
||||||
|
|
||||||
After installing and starting Gitea Mirror, you can configure it through the web interface:
|
|
||||||
|
|
||||||
1. Navigate to `http://your-server:port/`
|
|
||||||
2. If this is your first time, you'll be guided through creating an admin account
|
|
||||||
3. Log in with your credentials
|
|
||||||
4. Go to the Configuration page
|
|
||||||
|
|
||||||
### GitHub Configuration
|
|
||||||
|
|
||||||
The GitHub configuration section allows you to connect to GitHub and specify which repositories to mirror.
|
|
||||||
|
|
||||||
| Option | Description | Default |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| Username | Your GitHub username | - |
|
|
||||||
| Token | GitHub personal access token | - |
|
|
||||||
| Skip Forks | Skip forked repositories | `false` |
|
|
||||||
| Private Repositories | Include private repositories | `false` |
|
|
||||||
| Mirror Issues | Mirror issues from GitHub to Gitea | `false` |
|
|
||||||
| Mirror Starred | Mirror starred repositories | `false` |
|
|
||||||
| Mirror Organizations | Mirror organization repositories | `false` |
|
|
||||||
| Only Mirror Orgs | Only mirror organization repositories | `false` |
|
|
||||||
| Preserve Org Structure | Maintain organization structure in Gitea | `false` |
|
|
||||||
| Skip Starred Issues | Skip mirroring issues for starred repositories | `false` |
|
|
||||||
|
|
||||||
#### GitHub Token Permissions
|
|
||||||
|
|
||||||
Your GitHub token needs the following permissions:
|
|
||||||
|
|
||||||
- `repo` - Full control of private repositories
|
|
||||||
- `read:org` - Read organization membership
|
|
||||||
- `read:user` - Read user profile data
|
|
||||||
|
|
||||||
To create a GitHub token:
|
|
||||||
|
|
||||||
1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens)
|
|
||||||
2. Click "Generate new token"
|
|
||||||
3. Select the required permissions
|
|
||||||
4. Copy the generated token and paste it into Gitea Mirror
|
|
||||||
|
|
||||||
### Gitea Configuration
|
|
||||||
|
|
||||||
The Gitea configuration section allows you to connect to your Gitea instance and specify how repositories should be mirrored.
|
|
||||||
|
|
||||||
| Option | Description | Default |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| URL | Gitea server URL | - |
|
|
||||||
| Token | Gitea access token | - |
|
|
||||||
| Organization | Default organization for mirrored repositories | - |
|
|
||||||
| Visibility | Default visibility for mirrored repositories | `public` |
|
|
||||||
| Starred Repos Org | Organization for starred repositories | `github` |
|
|
||||||
|
|
||||||
#### Gitea Token Creation
|
|
||||||
|
|
||||||
To create a Gitea access token:
|
|
||||||
|
|
||||||
1. Log in to your Gitea instance
|
|
||||||
2. Go to Settings > Applications
|
|
||||||
3. Under "Generate New Token", enter a name for your token
|
|
||||||
4. Click "Generate Token"
|
|
||||||
5. Copy the generated token and paste it into Gitea Mirror
|
|
||||||
|
|
||||||
### Schedule Configuration
|
|
||||||
|
|
||||||
You can configure automatic mirroring on a schedule:
|
|
||||||
|
|
||||||
| Option | Description | Default |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| Enable Scheduling | Enable automatic mirroring | `false` |
|
|
||||||
| Interval (seconds) | Time between mirroring operations | `3600` (1 hour) |
|
|
||||||
|
|
||||||
## Advanced Configuration
|
|
||||||
|
|
||||||
### Repository Filtering
|
|
||||||
|
|
||||||
You can include or exclude specific repositories using patterns:
|
|
||||||
|
|
||||||
- Include patterns: Only repositories matching these patterns will be mirrored
|
|
||||||
- Exclude patterns: Repositories matching these patterns will be skipped
|
|
||||||
|
|
||||||
Example patterns:
|
|
||||||
- `*` - All repositories
|
|
||||||
- `org-name/*` - All repositories in a specific organization
|
|
||||||
- `username/repo-name` - A specific repository
|
|
||||||
|
|
||||||
### Database Management
|
|
||||||
|
|
||||||
Gitea Mirror includes several database management tools that can be run from the command line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Initialize the database (only if it doesn't exist)
|
|
||||||
bun run init-db
|
|
||||||
|
|
||||||
# Check database status
|
|
||||||
bun run check-db
|
|
||||||
|
|
||||||
# Fix database location issues
|
|
||||||
bun run fix-db
|
|
||||||
|
|
||||||
# Reset all users (for testing signup flow)
|
|
||||||
bun run reset-users
|
|
||||||
|
|
||||||
# Remove database files completely
|
|
||||||
bun run cleanup-db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Event Management
|
|
||||||
|
|
||||||
Events in Gitea Mirror (such as repository mirroring operations) are stored in the SQLite database. You can manage these events using the following scripts:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View all events in the database
|
|
||||||
bun scripts/check-events.ts
|
|
||||||
|
|
||||||
# Mark all events as read
|
|
||||||
bun scripts/mark-events-read.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
For cleaning up old activities and events, use the cleanup button in the Activity Log page of the web interface.
|
|
||||||
|
|
||||||
### Health Check Endpoint
|
|
||||||
|
|
||||||
Gitea Mirror includes a built-in health check endpoint at `/api/health` that provides:
|
|
||||||
|
|
||||||
- System status and uptime
|
|
||||||
- Database connectivity check
|
|
||||||
- Memory usage statistics
|
|
||||||
- Environment information
|
|
||||||
|
|
||||||
You can use this endpoint for monitoring your deployment:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Basic check (returns 200 OK if healthy)
|
|
||||||
curl -I http://your-server:port/api/health
|
|
||||||
|
|
||||||
# Detailed health information (JSON)
|
|
||||||
curl http://your-server:port/api/health
|
|
||||||
```
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Quick Start Guide"
|
|
||||||
description: "Get started with Gitea Mirror quickly."
|
|
||||||
order: 3
|
|
||||||
updatedDate: 2025-05-22
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="mb-6">
|
|
||||||
<h1 class="text-2xl font-bold text-foreground">Gitea Mirror Quick Start Guide</h1>
|
|
||||||
<p class="text-muted-foreground mt-2">This guide will help you get Gitea Mirror up and running quickly.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before you begin, make sure you have:
|
|
||||||
|
|
||||||
1. <span class="font-semibold text-foreground">A GitHub account with a personal access token</span>
|
|
||||||
2. <span class="font-semibold text-foreground">A Gitea instance with an access token</span>
|
|
||||||
3. <span class="font-semibold text-foreground">One of the following:</span>
|
|
||||||
- Docker and docker-compose (for Docker deployment)
|
|
||||||
- Bun 1.2.9+ (for native deployment)
|
|
||||||
- Proxmox VE or LXD (for LXC container deployment)
|
|
||||||
|
|
||||||
## Installation Options
|
|
||||||
|
|
||||||
Choose the installation method that works best for your environment.
|
|
||||||
|
|
||||||
### Using Docker (Recommended for most users)
|
|
||||||
|
|
||||||
Docker provides the easiest way to get started with minimal configuration.
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
|
||||||
cd gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Start the application in production mode:
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Access the application at [http://localhost:4321](http://localhost:4321)
|
|
||||||
|
|
||||||
### Using Bun (Native Installation)
|
|
||||||
|
|
||||||
If you prefer to run the application directly on your system:
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
|
||||||
cd gitea-mirror
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run the quick setup script:
|
|
||||||
```bash
|
|
||||||
bun run setup
|
|
||||||
```
|
|
||||||
This installs dependencies and initializes the database.
|
|
||||||
|
|
||||||
3. Choose how to run the application:
|
|
||||||
|
|
||||||
**Development Mode:**
|
|
||||||
```bash
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: For Bun-specific features, use:
|
|
||||||
```bash
|
|
||||||
bunx --bun astro dev
|
|
||||||
```
|
|
||||||
|
|
||||||
**Production Mode:**
|
|
||||||
```bash
|
|
||||||
bun run build
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Access the application at [http://localhost:4321](http://localhost:4321)
|
|
||||||
|
|
||||||
### Using LXC Containers (Recommended for server deployments)
|
|
||||||
|
|
||||||
#### Proxmox VE (Online Installation)
|
|
||||||
|
|
||||||
For deploying on a Proxmox VE host with internet access:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Optional env overrides: CTID HOSTNAME STORAGE DISK_SIZE CORES MEMORY BRIDGE IP_CONF
|
|
||||||
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-proxmox.sh)"
|
|
||||||
```
|
|
||||||
|
|
||||||
This script:
|
|
||||||
- Creates a privileged LXC container
|
|
||||||
- Installs Bun and dependencies
|
|
||||||
- Clones and builds the application
|
|
||||||
- Sets up a systemd service
|
|
||||||
|
|
||||||
#### Local LXD (Offline-friendly Installation)
|
|
||||||
|
|
||||||
For testing on a local workstation or in environments without internet access:
|
|
||||||
|
|
||||||
1. Clone the repository locally:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Download the Bun installer once:
|
|
||||||
```bash
|
|
||||||
curl -L -o /tmp/bun-linux-x64.zip https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Run the local LXC installer:
|
|
||||||
```bash
|
|
||||||
sudo LOCAL_REPO_DIR=~/path/to/gitea-mirror ./gitea-mirror/scripts/gitea-mirror-lxc-local.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
For more details on LXC deployment, see the [LXC Container Deployment Guide](https://github.com/arunavo4/gitea-mirror/blob/main/scripts/README-lxc.md).
|
|
||||||
|
|
||||||
## Initial Configuration
|
|
||||||
|
|
||||||
Follow these steps to configure Gitea Mirror for first use:
|
|
||||||
|
|
||||||
1. **Create Admin Account**
|
|
||||||
- Upon first access, you'll be prompted to create an admin account
|
|
||||||
- Choose a secure username and password
|
|
||||||
- This will be your administrator account
|
|
||||||
|
|
||||||
2. **Configure GitHub Connection**
|
|
||||||
- Navigate to the Configuration page
|
|
||||||
- Enter your GitHub username
|
|
||||||
- Enter your GitHub personal access token
|
|
||||||
- Select which repositories to mirror (all, starred, organizations)
|
|
||||||
- Configure repository filtering options
|
|
||||||
|
|
||||||
3. **Configure Gitea Connection**
|
|
||||||
- Enter your Gitea server URL
|
|
||||||
- Enter your Gitea access token
|
|
||||||
- Configure organization and visibility settings
|
|
||||||
|
|
||||||
4. **Set Up Scheduling (Optional)**
|
|
||||||
- Enable automatic mirroring if desired
|
|
||||||
- Set the mirroring interval (in seconds)
|
|
||||||
|
|
||||||
5. **Save Configuration**
|
|
||||||
- Click the "Save" button to store your settings
|
|
||||||
|
|
||||||
## Performing Your First Mirror
|
|
||||||
|
|
||||||
After completing the configuration, you can start mirroring repositories:
|
|
||||||
|
|
||||||
1. Click "Import GitHub Data" to fetch repositories from GitHub
|
|
||||||
2. Go to the Repositories page to view your imported repositories
|
|
||||||
3. Select the repositories you want to mirror
|
|
||||||
4. Click "Mirror Selected" to start the mirroring process
|
|
||||||
5. Monitor the progress on the Activity page
|
|
||||||
6. You'll receive toast notifications about the success or failure of operations
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
If you encounter any issues:
|
|
||||||
|
|
||||||
- Check the Activity Log for detailed error messages
|
|
||||||
- Verify your GitHub and Gitea tokens have the correct permissions
|
|
||||||
- Ensure your Gitea instance is accessible from the machine running Gitea Mirror
|
|
||||||
- Check logs based on your deployment method:
|
|
||||||
- Docker: `docker logs gitea-mirror`
|
|
||||||
- Native: Check the terminal output or system logs
|
|
||||||
- LXC: `systemctl status gitea-mirror` or `journalctl -u gitea-mirror -f`
|
|
||||||
- Use the health check endpoint to verify system status: `curl http://your-server:4321/api/health`
|
|
||||||
- For database issues, try the database management tools: `bun run check-db` or `bun run fix-db`
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
After your initial setup:
|
|
||||||
|
|
||||||
- Explore the dashboard for an overview of your mirroring status
|
|
||||||
- Set up automatic mirroring schedules for hands-off operation
|
|
||||||
- Configure organization mirroring for team repositories
|
|
||||||
- Check out the [Configuration Guide](/configuration) for advanced settings
|
|
||||||
- Review the [Architecture Documentation](/architecture) to understand the system
|
|
||||||
- For server deployments, set up monitoring using the health check endpoint
|
|
||||||
- Use the cleanup button in the Activity Log page to manage old events and activities
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { mirrorApi } from '@/lib/api';
|
|
||||||
import type { MirrorJob } from '@/lib/db/schema';
|
|
||||||
|
|
||||||
export function useMirror() {
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [currentJob, setCurrentJob] = useState<MirrorJob | null>(null);
|
|
||||||
const [jobs, setJobs] = useState<MirrorJob[]>([]);
|
|
||||||
|
|
||||||
const startMirror = async (configId: string, repositoryIds?: string[]) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const job = await mirrorApi.startMirror(configId, repositoryIds);
|
|
||||||
setCurrentJob(job);
|
|
||||||
return job;
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to start mirroring');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMirrorJobs = async (configId: string) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const fetchedJobs = await mirrorApi.getMirrorJobs(configId);
|
|
||||||
setJobs(fetchedJobs);
|
|
||||||
return fetchedJobs;
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch mirror jobs');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMirrorJob = async (jobId: string) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const job = await mirrorApi.getMirrorJob(jobId);
|
|
||||||
setCurrentJob(job);
|
|
||||||
return job;
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch mirror job');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelMirrorJob = async (jobId: string) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const result = await mirrorApi.cancelMirrorJob(jobId);
|
|
||||||
if (result.success && currentJob?.id === jobId) {
|
|
||||||
setCurrentJob({ ...currentJob, status: 'failed' });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to cancel mirror job');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
currentJob,
|
|
||||||
jobs,
|
|
||||||
startMirror,
|
|
||||||
getMirrorJobs,
|
|
||||||
getMirrorJob,
|
|
||||||
cancelMirrorJob,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -25,11 +25,256 @@ let sqlite: Database;
|
|||||||
try {
|
try {
|
||||||
sqlite = new Database(dbPath);
|
sqlite = new Database(dbPath);
|
||||||
console.log("Successfully connected to SQLite database using Bun's native driver");
|
console.log("Successfully connected to SQLite database using Bun's native driver");
|
||||||
|
|
||||||
|
// Ensure all required tables exist
|
||||||
|
ensureTablesExist(sqlite);
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
runMigrations(sqlite);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error opening database:", error);
|
console.error("Error opening database:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run database migrations
|
||||||
|
*/
|
||||||
|
function runMigrations(db: Database) {
|
||||||
|
try {
|
||||||
|
// Migration 1: Add destination_org column to organizations table
|
||||||
|
const orgTableInfo = db.query("PRAGMA table_info(organizations)").all() as Array<{name: string}>;
|
||||||
|
const hasDestinationOrg = orgTableInfo.some(col => col.name === 'destination_org');
|
||||||
|
|
||||||
|
if (!hasDestinationOrg) {
|
||||||
|
console.log("🔄 Running migration: Adding destination_org column to organizations table");
|
||||||
|
db.exec("ALTER TABLE organizations ADD COLUMN destination_org TEXT");
|
||||||
|
console.log("✅ Migration completed: destination_org column added");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration 2: Add destination_org column to repositories table
|
||||||
|
const repoTableInfo = db.query("PRAGMA table_info(repositories)").all() as Array<{name: string}>;
|
||||||
|
const hasRepoDestinationOrg = repoTableInfo.some(col => col.name === 'destination_org');
|
||||||
|
|
||||||
|
if (!hasRepoDestinationOrg) {
|
||||||
|
console.log("🔄 Running migration: Adding destination_org column to repositories table");
|
||||||
|
db.exec("ALTER TABLE repositories ADD COLUMN destination_org TEXT");
|
||||||
|
console.log("✅ Migration completed: destination_org column added to repositories");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Error running migrations:", error);
|
||||||
|
// Don't throw - migrations should be non-breaking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure all required tables exist in the database
|
||||||
|
*/
|
||||||
|
function ensureTablesExist(db: Database) {
|
||||||
|
const requiredTables = [
|
||||||
|
"users",
|
||||||
|
"configs",
|
||||||
|
"repositories",
|
||||||
|
"organizations",
|
||||||
|
"mirror_jobs",
|
||||||
|
"events",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const table of requiredTables) {
|
||||||
|
try {
|
||||||
|
// Check if table exists
|
||||||
|
const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`).get();
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.warn(`⚠️ Table '${table}' is missing. Creating it now...`);
|
||||||
|
createTable(db, table);
|
||||||
|
console.log(`✅ Table '${table}' created successfully`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error checking/creating table '${table}':`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a specific table with its schema
|
||||||
|
*/
|
||||||
|
function createTable(db: Database, tableName: string) {
|
||||||
|
switch (tableName) {
|
||||||
|
case "users":
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "configs":
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE configs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
|
github_config TEXT NOT NULL,
|
||||||
|
gitea_config TEXT NOT NULL,
|
||||||
|
include TEXT NOT NULL DEFAULT '["*"]',
|
||||||
|
exclude TEXT NOT NULL DEFAULT '[]',
|
||||||
|
schedule_config TEXT NOT NULL,
|
||||||
|
cleanup_config TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "repositories":
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE repositories (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
config_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
full_name TEXT NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
clone_url TEXT NOT NULL,
|
||||||
|
owner TEXT NOT NULL,
|
||||||
|
organization TEXT,
|
||||||
|
mirrored_location TEXT DEFAULT '',
|
||||||
|
is_private INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_fork INTEGER NOT NULL DEFAULT 0,
|
||||||
|
forked_from TEXT,
|
||||||
|
has_issues INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_starred INTEGER NOT NULL DEFAULT 0,
|
||||||
|
language TEXT,
|
||||||
|
description TEXT,
|
||||||
|
default_branch TEXT NOT NULL,
|
||||||
|
visibility TEXT NOT NULL DEFAULT 'public',
|
||||||
|
status TEXT NOT NULL DEFAULT 'imported',
|
||||||
|
last_mirrored INTEGER,
|
||||||
|
error_message TEXT,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create indexes for repositories
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_repositories_user_id ON repositories(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_repositories_config_id ON repositories(config_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_repositories_status ON repositories(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_repositories_owner ON repositories(owner);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_repositories_organization ON repositories(organization);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_repositories_is_fork ON repositories(is_fork);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_repositories_is_starred ON repositories(is_starred);
|
||||||
|
`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "organizations":
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE organizations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
config_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
avatar_url TEXT NOT NULL,
|
||||||
|
membership_role TEXT NOT NULL DEFAULT 'member',
|
||||||
|
is_included INTEGER NOT NULL DEFAULT 1,
|
||||||
|
status TEXT NOT NULL DEFAULT 'imported',
|
||||||
|
last_mirrored INTEGER,
|
||||||
|
error_message TEXT,
|
||||||
|
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
destination_org TEXT,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create indexes for organizations
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_organizations_user_id ON organizations(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_organizations_config_id ON organizations(config_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_organizations_status ON organizations(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_organizations_is_included ON organizations(is_included);
|
||||||
|
`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "mirror_jobs":
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE mirror_jobs (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
repository_id TEXT,
|
||||||
|
repository_name TEXT,
|
||||||
|
organization_id TEXT,
|
||||||
|
organization_name TEXT,
|
||||||
|
details TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'imported',
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
-- New fields for job resilience
|
||||||
|
job_type TEXT NOT NULL DEFAULT 'mirror',
|
||||||
|
batch_id TEXT,
|
||||||
|
total_items INTEGER,
|
||||||
|
completed_items INTEGER DEFAULT 0,
|
||||||
|
item_ids TEXT, -- JSON array as text
|
||||||
|
completed_item_ids TEXT DEFAULT '[]', -- JSON array as text
|
||||||
|
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer
|
||||||
|
started_at TIMESTAMP,
|
||||||
|
completed_at TIMESTAMP,
|
||||||
|
last_checkpoint TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create indexes for mirror_jobs
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp);
|
||||||
|
`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "events":
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE events (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
channel TEXT NOT NULL,
|
||||||
|
payload TEXT NOT NULL,
|
||||||
|
read INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create indexes for events
|
||||||
|
db.exec(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_events_read ON events(read);
|
||||||
|
`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown table: ${tableName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create drizzle instance with the SQLite client
|
// Create drizzle instance with the SQLite client
|
||||||
export const db = drizzle({ client: sqlite });
|
export const db = drizzle({ client: sqlite });
|
||||||
|
|
||||||
@@ -226,6 +471,9 @@ export const organizations = sqliteTable("organizations", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
|
||||||
|
// Override destination organization for this GitHub org's repos
|
||||||
|
destinationOrg: text("destination_org"),
|
||||||
|
|
||||||
status: text("status").notNull().default("imported"),
|
status: text("status").notNull().default("imported"),
|
||||||
lastMirrored: integer("last_mirrored", { mode: "timestamp" }),
|
lastMirrored: integer("last_mirrored", { mode: "timestamp" }),
|
||||||
errorMessage: text("error_message"),
|
errorMessage: text("error_message"),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const configSchema = z.object({
|
|||||||
skipForks: z.boolean().default(false),
|
skipForks: z.boolean().default(false),
|
||||||
privateRepositories: z.boolean().default(false),
|
privateRepositories: z.boolean().default(false),
|
||||||
mirrorIssues: z.boolean().default(false),
|
mirrorIssues: z.boolean().default(false),
|
||||||
|
mirrorWiki: z.boolean().default(false),
|
||||||
mirrorStarred: z.boolean().default(false),
|
mirrorStarred: z.boolean().default(false),
|
||||||
useSpecificUser: z.boolean().default(false),
|
useSpecificUser: z.boolean().default(false),
|
||||||
singleRepo: z.string().optional(),
|
singleRepo: z.string().optional(),
|
||||||
@@ -33,7 +34,6 @@ export const configSchema = z.object({
|
|||||||
excludeOrgs: z.array(z.string()).default([]),
|
excludeOrgs: z.array(z.string()).default([]),
|
||||||
mirrorPublicOrgs: z.boolean().default(false),
|
mirrorPublicOrgs: z.boolean().default(false),
|
||||||
publicOrgs: z.array(z.string()).default([]),
|
publicOrgs: z.array(z.string()).default([]),
|
||||||
preserveOrgStructure: z.boolean().default(false),
|
|
||||||
skipStarredIssues: z.boolean().default(false),
|
skipStarredIssues: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
giteaConfig: z.object({
|
giteaConfig: z.object({
|
||||||
@@ -43,6 +43,9 @@ export const configSchema = z.object({
|
|||||||
organization: z.string().optional(),
|
organization: z.string().optional(),
|
||||||
visibility: z.enum(["public", "private", "limited"]).default("public"),
|
visibility: z.enum(["public", "private", "limited"]).default("public"),
|
||||||
starredReposOrg: z.string().default("github"),
|
starredReposOrg: z.string().default("github"),
|
||||||
|
preserveOrgStructure: z.boolean().default(false),
|
||||||
|
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).optional(),
|
||||||
|
personalReposOrg: z.string().optional(), // Override destination for personal repos
|
||||||
}),
|
}),
|
||||||
include: z.array(z.string()).default(["*"]),
|
include: z.array(z.string()).default(["*"]),
|
||||||
exclude: z.array(z.string()).default([]),
|
exclude: z.array(z.string()).default([]),
|
||||||
@@ -98,6 +101,7 @@ export const repositorySchema = z.object({
|
|||||||
errorMessage: z.string().optional(),
|
errorMessage: z.string().optional(),
|
||||||
|
|
||||||
mirroredLocation: z.string().default(""), // Store the full Gitea path where repo was mirrored
|
mirroredLocation: z.string().default(""), // Store the full Gitea path where repo was mirrored
|
||||||
|
destinationOrg: z.string().optional(), // Custom destination organization override
|
||||||
|
|
||||||
createdAt: z.date().default(() => new Date()),
|
createdAt: z.date().default(() => new Date()),
|
||||||
updatedAt: z.date().default(() => new Date()),
|
updatedAt: z.date().default(() => new Date()),
|
||||||
@@ -152,6 +156,12 @@ export const organizationSchema = z.object({
|
|||||||
errorMessage: z.string().optional(),
|
errorMessage: z.string().optional(),
|
||||||
|
|
||||||
repositoryCount: z.number().default(0),
|
repositoryCount: z.number().default(0),
|
||||||
|
publicRepositoryCount: z.number().optional(),
|
||||||
|
privateRepositoryCount: z.number().optional(),
|
||||||
|
forkRepositoryCount: z.number().optional(),
|
||||||
|
|
||||||
|
// Override destination organization for this GitHub org's repos
|
||||||
|
destinationOrg: z.string().optional(),
|
||||||
|
|
||||||
createdAt: z.date().default(() => new Date()),
|
createdAt: z.date().default(() => new Date()),
|
||||||
updatedAt: z.date().default(() => new Date()),
|
updatedAt: z.date().default(() => new Date()),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { repoStatusEnum } from "@/types/Repository";
|
import { repoStatusEnum } from "@/types/Repository";
|
||||||
import { getOrCreateGiteaOrg } from "./gitea";
|
import { getOrCreateGiteaOrg, getGiteaRepoOwner, getGiteaRepoOwnerAsync } from "./gitea";
|
||||||
|
import type { Config, Repository, Organization } from "./db/schema";
|
||||||
|
|
||||||
// Mock the isRepoPresentInGitea function
|
// Mock the isRepoPresentInGitea function
|
||||||
const mockIsRepoPresentInGitea = mock(() => Promise.resolve(false));
|
const mockIsRepoPresentInGitea = mock(() => Promise.resolve(false));
|
||||||
@@ -204,4 +205,220 @@ describe("Gitea Repository Mirroring", () => {
|
|||||||
global.fetch = originalFetch;
|
global.fetch = originalFetch;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mirrorGitHubOrgToGitea handles empty organizations correctly", async () => {
|
||||||
|
// Mock the createMirrorJob function
|
||||||
|
const mockCreateMirrorJob = mock(() => Promise.resolve("job-id"));
|
||||||
|
|
||||||
|
// Mock the getOrCreateGiteaOrg function
|
||||||
|
const mockGetOrCreateGiteaOrg = mock(() => Promise.resolve("gitea-org-id"));
|
||||||
|
|
||||||
|
// Create a test version of the function with mocked dependencies
|
||||||
|
const testMirrorGitHubOrgToGitea = async ({
|
||||||
|
organization,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
organization: any;
|
||||||
|
config: any;
|
||||||
|
}) => {
|
||||||
|
// Simulate the function logic for empty organization
|
||||||
|
console.log(`Mirroring organization ${organization.name}`);
|
||||||
|
|
||||||
|
// Mock: get or create Gitea org
|
||||||
|
await mockGetOrCreateGiteaOrg();
|
||||||
|
|
||||||
|
// Mock: query the db with the org name and get the repos
|
||||||
|
const orgRepos: any[] = []; // Empty array to simulate no repositories
|
||||||
|
|
||||||
|
if (orgRepos.length === 0) {
|
||||||
|
console.log(`No repositories found for organization ${organization.name} - marking as successfully mirrored`);
|
||||||
|
} else {
|
||||||
|
console.log(`Mirroring ${orgRepos.length} repositories for organization ${organization.name}`);
|
||||||
|
// Repository processing would happen here
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Organization ${organization.name} mirrored successfully`);
|
||||||
|
|
||||||
|
// Mock: Append log for "mirrored" status
|
||||||
|
await mockCreateMirrorJob({
|
||||||
|
userId: config.userId,
|
||||||
|
organizationId: organization.id,
|
||||||
|
organizationName: organization.name,
|
||||||
|
message: `Successfully mirrored organization: ${organization.name}`,
|
||||||
|
details: orgRepos.length === 0
|
||||||
|
? `Organization ${organization.name} was processed successfully (no repositories found).`
|
||||||
|
: `Organization ${organization.name} was mirrored to Gitea with ${orgRepos.length} repositories.`,
|
||||||
|
status: "mirrored",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create mock organization
|
||||||
|
const organization = {
|
||||||
|
id: "org-id",
|
||||||
|
name: "empty-org",
|
||||||
|
status: "imported"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create mock config
|
||||||
|
const config = {
|
||||||
|
id: "config-id",
|
||||||
|
userId: "user-id",
|
||||||
|
githubConfig: {
|
||||||
|
token: "github-token"
|
||||||
|
},
|
||||||
|
giteaConfig: {
|
||||||
|
url: "https://gitea.example.com",
|
||||||
|
token: "gitea-token"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the test function
|
||||||
|
await testMirrorGitHubOrgToGitea({
|
||||||
|
organization,
|
||||||
|
config
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that the mirror job was created with the correct details for empty org
|
||||||
|
expect(mockCreateMirrorJob).toHaveBeenCalledWith({
|
||||||
|
userId: "user-id",
|
||||||
|
organizationId: "org-id",
|
||||||
|
organizationName: "empty-org",
|
||||||
|
message: "Successfully mirrored organization: empty-org",
|
||||||
|
details: "Organization empty-org was processed successfully (no repositories found).",
|
||||||
|
status: "mirrored",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that getOrCreateGiteaOrg was called
|
||||||
|
expect(mockGetOrCreateGiteaOrg).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||||
|
const baseConfig: Partial<Config> = {
|
||||||
|
githubConfig: {
|
||||||
|
username: "testuser",
|
||||||
|
token: "token",
|
||||||
|
preserveOrgStructure: false,
|
||||||
|
skipForks: false,
|
||||||
|
privateRepositories: false,
|
||||||
|
mirrorIssues: false,
|
||||||
|
mirrorWiki: false,
|
||||||
|
mirrorStarred: false,
|
||||||
|
useSpecificUser: false,
|
||||||
|
includeOrgs: [],
|
||||||
|
excludeOrgs: [],
|
||||||
|
mirrorPublicOrgs: false,
|
||||||
|
publicOrgs: [],
|
||||||
|
skipStarredIssues: false
|
||||||
|
},
|
||||||
|
giteaConfig: {
|
||||||
|
username: "giteauser",
|
||||||
|
url: "https://gitea.example.com",
|
||||||
|
token: "gitea-token",
|
||||||
|
organization: "github-mirrors",
|
||||||
|
visibility: "public",
|
||||||
|
starredReposOrg: "starred",
|
||||||
|
preserveOrgStructure: false,
|
||||||
|
mirrorStrategy: "preserve"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseRepo: Repository = {
|
||||||
|
id: "repo-id",
|
||||||
|
userId: "user-id",
|
||||||
|
configId: "config-id",
|
||||||
|
name: "test-repo",
|
||||||
|
fullName: "testuser/test-repo",
|
||||||
|
url: "https://github.com/testuser/test-repo",
|
||||||
|
cloneUrl: "https://github.com/testuser/test-repo.git",
|
||||||
|
owner: "testuser",
|
||||||
|
isPrivate: false,
|
||||||
|
isForked: false,
|
||||||
|
hasIssues: true,
|
||||||
|
isStarred: false,
|
||||||
|
isArchived: false,
|
||||||
|
size: 1000,
|
||||||
|
hasLFS: false,
|
||||||
|
hasSubmodules: false,
|
||||||
|
defaultBranch: "main",
|
||||||
|
visibility: "public",
|
||||||
|
status: "imported",
|
||||||
|
mirroredLocation: "",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
test("starred repos go to starredReposOrg", () => {
|
||||||
|
const repo = { ...baseRepo, isStarred: true };
|
||||||
|
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||||
|
expect(result).toBe("starred");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserve strategy: personal repos use personalReposOrg override", () => {
|
||||||
|
const configWithOverride = {
|
||||||
|
...baseConfig,
|
||||||
|
giteaConfig: {
|
||||||
|
...baseConfig.giteaConfig!,
|
||||||
|
personalReposOrg: "my-personal-mirrors"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const repo = { ...baseRepo, organization: undefined };
|
||||||
|
const result = getGiteaRepoOwner({ config: configWithOverride, repository: repo });
|
||||||
|
expect(result).toBe("my-personal-mirrors");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserve strategy: personal repos fallback to username when no override", () => {
|
||||||
|
const repo = { ...baseRepo, organization: undefined };
|
||||||
|
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||||
|
expect(result).toBe("giteauser");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserve strategy: org repos go to same org name", () => {
|
||||||
|
const repo = { ...baseRepo, organization: "myorg" };
|
||||||
|
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||||
|
expect(result).toBe("myorg");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mixed strategy: personal repos go to organization", () => {
|
||||||
|
const configWithMixed = {
|
||||||
|
...baseConfig,
|
||||||
|
giteaConfig: {
|
||||||
|
...baseConfig.giteaConfig!,
|
||||||
|
mirrorStrategy: "mixed" as const,
|
||||||
|
organization: "github-mirrors"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const repo = { ...baseRepo, organization: undefined };
|
||||||
|
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
||||||
|
expect(result).toBe("github-mirrors");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mixed strategy: org repos preserve their structure", () => {
|
||||||
|
const configWithMixed = {
|
||||||
|
...baseConfig,
|
||||||
|
giteaConfig: {
|
||||||
|
...baseConfig.giteaConfig!,
|
||||||
|
mirrorStrategy: "mixed" as const,
|
||||||
|
organization: "github-mirrors"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const repo = { ...baseRepo, organization: "myorg" };
|
||||||
|
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
||||||
|
expect(result).toBe("myorg");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mixed strategy: fallback to username if no org configs", () => {
|
||||||
|
const configWithMixed = {
|
||||||
|
...baseConfig,
|
||||||
|
giteaConfig: {
|
||||||
|
...baseConfig.giteaConfig!,
|
||||||
|
mirrorStrategy: "mixed" as const,
|
||||||
|
organization: undefined,
|
||||||
|
personalReposOrg: undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const repo = { ...baseRepo, organization: undefined };
|
||||||
|
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
||||||
|
expect(result).toBe("giteauser");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
610
src/lib/gitea.ts
@@ -9,7 +9,87 @@ import type { Organization, Repository } from "./db/schema";
|
|||||||
import { httpPost, httpGet } from "./http-client";
|
import { httpPost, httpGet } from "./http-client";
|
||||||
import { createMirrorJob } from "./helpers";
|
import { createMirrorJob } from "./helpers";
|
||||||
import { db, organizations, repositories } from "./db";
|
import { db, organizations, repositories } from "./db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get organization configuration including destination override
|
||||||
|
*/
|
||||||
|
export const getOrganizationConfig = async ({
|
||||||
|
orgName,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
orgName: string;
|
||||||
|
userId: string;
|
||||||
|
}): Promise<Organization | null> => {
|
||||||
|
try {
|
||||||
|
const [orgConfig] = await db
|
||||||
|
.select()
|
||||||
|
.from(organizations)
|
||||||
|
.where(and(eq(organizations.name, orgName), eq(organizations.userId, userId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return orgConfig || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching organization config for ${orgName}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced async version of getGiteaRepoOwner that supports organization overrides
|
||||||
|
*/
|
||||||
|
export const getGiteaRepoOwnerAsync = async ({
|
||||||
|
config,
|
||||||
|
repository,
|
||||||
|
}: {
|
||||||
|
config: Partial<Config>;
|
||||||
|
repository: Repository;
|
||||||
|
}): Promise<string> => {
|
||||||
|
if (!config.githubConfig || !config.giteaConfig) {
|
||||||
|
throw new Error("GitHub or Gitea config is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.giteaConfig.username) {
|
||||||
|
throw new Error("Gitea username is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.userId) {
|
||||||
|
throw new Error("User ID is required for organization overrides.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if repository is starred - starred repos always go to starredReposOrg (highest priority)
|
||||||
|
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
||||||
|
return config.giteaConfig.starredReposOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for repository-specific override (second highest priority)
|
||||||
|
if (repository.destinationOrg) {
|
||||||
|
console.log(`Using repository override: ${repository.fullName} -> ${repository.destinationOrg}`);
|
||||||
|
return repository.destinationOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for organization-specific override
|
||||||
|
if (repository.organization) {
|
||||||
|
const orgConfig = await getOrganizationConfig({
|
||||||
|
orgName: repository.organization,
|
||||||
|
userId: config.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orgConfig?.destinationOrg) {
|
||||||
|
console.log(`Using organization override: ${repository.organization} -> ${orgConfig.destinationOrg}`);
|
||||||
|
return orgConfig.destinationOrg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for personal repos override (when it's user's repo, not an organization)
|
||||||
|
if (!repository.organization && config.giteaConfig.personalReposOrg) {
|
||||||
|
console.log(`Using personal repos override: ${config.giteaConfig.personalReposOrg}`);
|
||||||
|
return config.giteaConfig.personalReposOrg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to existing strategy logic
|
||||||
|
return getGiteaRepoOwner({ config, repository });
|
||||||
|
};
|
||||||
|
|
||||||
export const getGiteaRepoOwner = ({
|
export const getGiteaRepoOwner = ({
|
||||||
config,
|
config,
|
||||||
@@ -26,13 +106,53 @@ export const getGiteaRepoOwner = ({
|
|||||||
throw new Error("Gitea username is required.");
|
throw new Error("Gitea username is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the config has preserveOrgStructure set to true, then use the org name as the owner
|
// Check if repository is starred - starred repos always go to starredReposOrg
|
||||||
if (config.githubConfig.preserveOrgStructure && repository.organization) {
|
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
||||||
return repository.organization;
|
return config.giteaConfig.starredReposOrg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the config has preserveOrgStructure set to false, then use the gitea username as the owner
|
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
|
||||||
return config.giteaConfig.username;
|
const mirrorStrategy = config.giteaConfig.mirrorStrategy ||
|
||||||
|
(config.githubConfig.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
|
|
||||||
|
switch (mirrorStrategy) {
|
||||||
|
case "preserve":
|
||||||
|
// Keep GitHub structure - org repos go to same org, personal repos to user (or override)
|
||||||
|
if (repository.organization) {
|
||||||
|
return repository.organization;
|
||||||
|
}
|
||||||
|
// Use personal repos override if configured, otherwise use username
|
||||||
|
return config.giteaConfig.personalReposOrg || config.giteaConfig.username;
|
||||||
|
|
||||||
|
case "single-org":
|
||||||
|
// All non-starred repos go to the destination organization
|
||||||
|
if (config.giteaConfig.organization) {
|
||||||
|
return config.giteaConfig.organization;
|
||||||
|
}
|
||||||
|
// Fallback to username if no organization specified
|
||||||
|
return config.giteaConfig.username;
|
||||||
|
|
||||||
|
case "flat-user":
|
||||||
|
// All non-starred repos go under the user account
|
||||||
|
return config.giteaConfig.username;
|
||||||
|
|
||||||
|
case "mixed":
|
||||||
|
// Mixed mode: personal repos to single org, organization repos preserve structure
|
||||||
|
if (repository.organization) {
|
||||||
|
// Organization repos preserve their structure
|
||||||
|
return repository.organization;
|
||||||
|
}
|
||||||
|
// Personal repos go to configured organization (same as single-org)
|
||||||
|
if (config.giteaConfig.organization) {
|
||||||
|
return config.giteaConfig.organization;
|
||||||
|
}
|
||||||
|
// Fallback to username if no organization specified
|
||||||
|
return config.giteaConfig.username;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Default fallback
|
||||||
|
return config.giteaConfig.username;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isRepoPresentInGitea = async ({
|
export const isRepoPresentInGitea = async ({
|
||||||
@@ -80,8 +200,11 @@ export const checkRepoLocation = async ({
|
|||||||
expectedOwner: string;
|
expectedOwner: string;
|
||||||
}): Promise<{ present: boolean; actualOwner: string }> => {
|
}): Promise<{ present: boolean; actualOwner: string }> => {
|
||||||
// First check if we have a recorded mirroredLocation and if the repo exists there
|
// First check if we have a recorded mirroredLocation and if the repo exists there
|
||||||
if (repository.mirroredLocation && repository.mirroredLocation.trim() !== "") {
|
if (
|
||||||
const [mirroredOwner] = repository.mirroredLocation.split('/');
|
repository.mirroredLocation &&
|
||||||
|
repository.mirroredLocation.trim() !== ""
|
||||||
|
) {
|
||||||
|
const [mirroredOwner] = repository.mirroredLocation.split("/");
|
||||||
if (mirroredOwner) {
|
if (mirroredOwner) {
|
||||||
const mirroredPresent = await isRepoPresentInGitea({
|
const mirroredPresent = await isRepoPresentInGitea({
|
||||||
config,
|
config,
|
||||||
@@ -90,7 +213,9 @@ export const checkRepoLocation = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (mirroredPresent) {
|
if (mirroredPresent) {
|
||||||
console.log(`Repository found at recorded mirrored location: ${repository.mirroredLocation}`);
|
console.log(
|
||||||
|
`Repository found at recorded mirrored location: ${repository.mirroredLocation}`
|
||||||
|
);
|
||||||
return { present: true, actualOwner: mirroredOwner };
|
return { present: true, actualOwner: mirroredOwner };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,15 +254,44 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
throw new Error("Gitea username is required.");
|
throw new Error("Gitea username is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the correct owner based on the strategy (with organization overrides)
|
||||||
|
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
||||||
|
|
||||||
const isExisting = await isRepoPresentInGitea({
|
const isExisting = await isRepoPresentInGitea({
|
||||||
config,
|
config,
|
||||||
owner: config.giteaConfig.username,
|
owner: repoOwner,
|
||||||
repoName: repository.name,
|
repoName: repository.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
console.log(
|
console.log(
|
||||||
`Repository ${repository.name} already exists in Gitea. Skipping migration.`
|
`Repository ${repository.name} already exists in Gitea under ${repoOwner}. Updating database status.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update database to reflect that the repository is already mirrored
|
||||||
|
await db
|
||||||
|
.update(repositories)
|
||||||
|
.set({
|
||||||
|
status: repoStatusEnum.parse("mirrored"),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
lastMirrored: new Date(),
|
||||||
|
errorMessage: null,
|
||||||
|
mirroredLocation: `${repoOwner}/${repository.name}`,
|
||||||
|
})
|
||||||
|
.where(eq(repositories.id, repository.id!));
|
||||||
|
|
||||||
|
// Append log for "mirrored" status
|
||||||
|
await createMirrorJob({
|
||||||
|
userId: config.userId,
|
||||||
|
repositoryId: repository.id,
|
||||||
|
repositoryName: repository.name,
|
||||||
|
message: `Repository ${repository.name} already exists in Gitea`,
|
||||||
|
details: `Repository ${repository.name} was found to already exist in Gitea under ${repoOwner} and database status was updated.`,
|
||||||
|
status: "mirrored",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Repository ${repository.name} database status updated to mirrored`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,20 +335,45 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
|
|
||||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||||
|
|
||||||
const response = await httpPost(apiUrl, {
|
// Handle organization creation if needed for single-org or preserve strategies
|
||||||
clone_addr: cloneAddress,
|
if (repoOwner !== config.giteaConfig.username && !repository.isStarred) {
|
||||||
repo_name: repository.name,
|
// Need to create the organization if it doesn't exist
|
||||||
mirror: true,
|
await getOrCreateGiteaOrg({
|
||||||
private: repository.isPrivate,
|
orgName: repoOwner,
|
||||||
repo_owner: config.giteaConfig.username,
|
config,
|
||||||
description: "",
|
});
|
||||||
service: "git",
|
}
|
||||||
}, {
|
|
||||||
"Authorization": `token ${config.giteaConfig.token}`,
|
const response = await httpPost(
|
||||||
|
apiUrl,
|
||||||
|
{
|
||||||
|
clone_addr: cloneAddress,
|
||||||
|
repo_name: repository.name,
|
||||||
|
mirror: true,
|
||||||
|
wiki: config.githubConfig.mirrorWiki || false, // will mirror wiki if it exists
|
||||||
|
private: repository.isPrivate,
|
||||||
|
repo_owner: repoOwner,
|
||||||
|
description: "",
|
||||||
|
service: "git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Authorization: `token ${config.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
//mirror releases
|
||||||
|
await mirrorGitHubReleasesToGitea({
|
||||||
|
config,
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
});
|
});
|
||||||
|
|
||||||
// clone issues
|
// clone issues
|
||||||
if (config.githubConfig.mirrorIssues) {
|
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||||
|
const shouldMirrorIssues = config.githubConfig.mirrorIssues &&
|
||||||
|
!(repository.isStarred && config.githubConfig.skipStarredIssues);
|
||||||
|
|
||||||
|
if (shouldMirrorIssues) {
|
||||||
await mirrorGitRepoIssuesToGitea({
|
await mirrorGitRepoIssuesToGitea({
|
||||||
config,
|
config,
|
||||||
octokit,
|
octokit,
|
||||||
@@ -213,7 +392,7 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
lastMirrored: new Date(),
|
lastMirrored: new Date(),
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
mirroredLocation: `${config.giteaConfig.username}/${repository.name}`,
|
mirroredLocation: `${repoOwner}/${repository.name}`,
|
||||||
})
|
})
|
||||||
.where(eq(repositories.id, repository.id!));
|
.where(eq(repositories.id, repository.id!));
|
||||||
|
|
||||||
@@ -293,16 +472,22 @@ export async function getOrCreateGiteaOrg({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Get org response status: ${orgRes.status} for org: ${orgName}`);
|
console.log(
|
||||||
|
`Get org response status: ${orgRes.status} for org: ${orgName}`
|
||||||
|
);
|
||||||
|
|
||||||
if (orgRes.ok) {
|
if (orgRes.ok) {
|
||||||
// Check if response is actually JSON
|
// Check if response is actually JSON
|
||||||
const contentType = orgRes.headers.get("content-type");
|
const contentType = orgRes.headers.get("content-type");
|
||||||
if (!contentType || !contentType.includes("application/json")) {
|
if (!contentType || !contentType.includes("application/json")) {
|
||||||
console.warn(`Expected JSON response but got content-type: ${contentType}`);
|
console.warn(
|
||||||
|
`Expected JSON response but got content-type: ${contentType}`
|
||||||
|
);
|
||||||
const responseText = await orgRes.text();
|
const responseText = await orgRes.text();
|
||||||
console.warn(`Response body: ${responseText}`);
|
console.warn(`Response body: ${responseText}`);
|
||||||
throw new Error(`Invalid response format from Gitea API. Expected JSON but got: ${contentType}`);
|
throw new Error(
|
||||||
|
`Invalid response format from Gitea API. Expected JSON but got: ${contentType}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone the response to handle potential JSON parsing errors
|
// Clone the response to handle potential JSON parsing errors
|
||||||
@@ -310,14 +495,22 @@ export async function getOrCreateGiteaOrg({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const org = await orgRes.json();
|
const org = await orgRes.json();
|
||||||
console.log(`Successfully retrieved existing org: ${orgName} with ID: ${org.id}`);
|
console.log(
|
||||||
|
`Successfully retrieved existing org: ${orgName} with ID: ${org.id}`
|
||||||
|
);
|
||||||
// Note: Organization events are handled by the main mirroring process
|
// Note: Organization events are handled by the main mirroring process
|
||||||
// to avoid duplicate events
|
// to avoid duplicate events
|
||||||
return org.id;
|
return org.id;
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
const responseText = await orgResClone.text();
|
const responseText = await orgResClone.text();
|
||||||
console.error(`Failed to parse JSON response for existing org: ${responseText}`);
|
console.error(
|
||||||
throw new Error(`Failed to parse JSON response from Gitea API: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`);
|
`Failed to parse JSON response for existing org: ${responseText}`
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse JSON response from Gitea API: ${
|
||||||
|
jsonError instanceof Error ? jsonError.message : String(jsonError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,25 +526,33 @@ export async function getOrCreateGiteaOrg({
|
|||||||
username: orgName,
|
username: orgName,
|
||||||
full_name: `${orgName} Org`,
|
full_name: `${orgName} Org`,
|
||||||
description: `Mirrored organization from GitHub ${orgName}`,
|
description: `Mirrored organization from GitHub ${orgName}`,
|
||||||
visibility: "public",
|
visibility: config.giteaConfig?.visibility || "public",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Create org response status: ${createRes.status} for org: ${orgName}`);
|
console.log(
|
||||||
|
`Create org response status: ${createRes.status} for org: ${orgName}`
|
||||||
|
);
|
||||||
|
|
||||||
if (!createRes.ok) {
|
if (!createRes.ok) {
|
||||||
const errorText = await createRes.text();
|
const errorText = await createRes.text();
|
||||||
console.error(`Failed to create org ${orgName}. Status: ${createRes.status}, Response: ${errorText}`);
|
console.error(
|
||||||
|
`Failed to create org ${orgName}. Status: ${createRes.status}, Response: ${errorText}`
|
||||||
|
);
|
||||||
throw new Error(`Failed to create Gitea org: ${errorText}`);
|
throw new Error(`Failed to create Gitea org: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if response is actually JSON
|
// Check if response is actually JSON
|
||||||
const createContentType = createRes.headers.get("content-type");
|
const createContentType = createRes.headers.get("content-type");
|
||||||
if (!createContentType || !createContentType.includes("application/json")) {
|
if (!createContentType || !createContentType.includes("application/json")) {
|
||||||
console.warn(`Expected JSON response but got content-type: ${createContentType}`);
|
console.warn(
|
||||||
|
`Expected JSON response but got content-type: ${createContentType}`
|
||||||
|
);
|
||||||
const responseText = await createRes.text();
|
const responseText = await createRes.text();
|
||||||
console.warn(`Response body: ${responseText}`);
|
console.warn(`Response body: ${responseText}`);
|
||||||
throw new Error(`Invalid response format from Gitea API. Expected JSON but got: ${createContentType}`);
|
throw new Error(
|
||||||
|
`Invalid response format from Gitea API. Expected JSON but got: ${createContentType}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Organization creation events are handled by the main mirroring process
|
// Note: Organization creation events are handled by the main mirroring process
|
||||||
@@ -362,12 +563,20 @@ export async function getOrCreateGiteaOrg({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const newOrg = await createRes.json();
|
const newOrg = await createRes.json();
|
||||||
console.log(`Successfully created new org: ${orgName} with ID: ${newOrg.id}`);
|
console.log(
|
||||||
|
`Successfully created new org: ${orgName} with ID: ${newOrg.id}`
|
||||||
|
);
|
||||||
return newOrg.id;
|
return newOrg.id;
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
const responseText = await createResClone.text();
|
const responseText = await createResClone.text();
|
||||||
console.error(`Failed to parse JSON response for new org: ${responseText}`);
|
console.error(
|
||||||
throw new Error(`Failed to parse JSON response from Gitea API: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`);
|
`Failed to parse JSON response for new org: ${responseText}`
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse JSON response from Gitea API: ${
|
||||||
|
jsonError instanceof Error ? jsonError.message : String(jsonError)
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@@ -375,7 +584,9 @@ export async function getOrCreateGiteaOrg({
|
|||||||
? error.message
|
? error.message
|
||||||
: "Unknown error occurred in getOrCreateGiteaOrg.";
|
: "Unknown error occurred in getOrCreateGiteaOrg.";
|
||||||
|
|
||||||
console.error(`Error in getOrCreateGiteaOrg for ${orgName}: ${errorMessage}`);
|
console.error(
|
||||||
|
`Error in getOrCreateGiteaOrg for ${orgName}: ${errorMessage}`
|
||||||
|
);
|
||||||
|
|
||||||
await createMirrorJob({
|
await createMirrorJob({
|
||||||
userId: config.userId,
|
userId: config.userId,
|
||||||
@@ -420,7 +631,33 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
|
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
console.log(
|
console.log(
|
||||||
`Repository ${repository.name} already exists in Gitea. Skipping migration.`
|
`Repository ${repository.name} already exists in Gitea organization ${orgName}. Updating database status.`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update database to reflect that the repository is already mirrored
|
||||||
|
await db
|
||||||
|
.update(repositories)
|
||||||
|
.set({
|
||||||
|
status: repoStatusEnum.parse("mirrored"),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
lastMirrored: new Date(),
|
||||||
|
errorMessage: null,
|
||||||
|
mirroredLocation: `${orgName}/${repository.name}`,
|
||||||
|
})
|
||||||
|
.where(eq(repositories.id, repository.id!));
|
||||||
|
|
||||||
|
// Create a mirror job log entry
|
||||||
|
await createMirrorJob({
|
||||||
|
userId: config.userId,
|
||||||
|
repositoryId: repository.id,
|
||||||
|
repositoryName: repository.name,
|
||||||
|
message: `Repository ${repository.name} already exists in Gitea organization ${orgName}`,
|
||||||
|
details: `Repository ${repository.name} was found to already exist in Gitea organization ${orgName} and database status was updated.`,
|
||||||
|
status: "mirrored",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Repository ${repository.name} database status updated to mirrored in organization ${orgName}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -458,18 +695,34 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
|
|
||||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||||
|
|
||||||
const migrateRes = await httpPost(apiUrl, {
|
const migrateRes = await httpPost(
|
||||||
clone_addr: cloneAddress,
|
apiUrl,
|
||||||
uid: giteaOrgId,
|
{
|
||||||
repo_name: repository.name,
|
clone_addr: cloneAddress,
|
||||||
mirror: true,
|
uid: giteaOrgId,
|
||||||
private: repository.isPrivate,
|
repo_name: repository.name,
|
||||||
}, {
|
mirror: true,
|
||||||
"Authorization": `token ${config.giteaConfig.token}`,
|
wiki: config.githubConfig?.mirrorWiki || false, // will mirror wiki if it exists
|
||||||
|
private: repository.isPrivate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Authorization: `token ${config.giteaConfig.token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
//mirror releases
|
||||||
|
await mirrorGitHubReleasesToGitea({
|
||||||
|
config,
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clone issues
|
// Clone issues
|
||||||
if (config.githubConfig?.mirrorIssues) {
|
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||||
|
const shouldMirrorIssues = config.githubConfig?.mirrorIssues &&
|
||||||
|
!(repository.isStarred && config.githubConfig?.skipStarredIssues);
|
||||||
|
|
||||||
|
if (shouldMirrorIssues) {
|
||||||
await mirrorGitRepoIssuesToGitea({
|
await mirrorGitRepoIssuesToGitea({
|
||||||
config,
|
config,
|
||||||
octokit,
|
octokit,
|
||||||
@@ -616,11 +869,37 @@ export async function mirrorGitHubOrgToGitea({
|
|||||||
status: repoStatusEnum.parse("mirroring"),
|
status: repoStatusEnum.parse("mirroring"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const giteaOrgId = await getOrCreateGiteaOrg({
|
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
|
||||||
orgId: organization.id,
|
const mirrorStrategy = config.giteaConfig?.mirrorStrategy ||
|
||||||
orgName: organization.name,
|
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
config,
|
|
||||||
});
|
let giteaOrgId: number;
|
||||||
|
let targetOrgName: string;
|
||||||
|
|
||||||
|
// Determine the target organization based on strategy
|
||||||
|
if (mirrorStrategy === "single-org" && config.giteaConfig?.organization) {
|
||||||
|
// For single-org strategy, use the configured destination organization
|
||||||
|
targetOrgName = config.giteaConfig.organization;
|
||||||
|
giteaOrgId = await getOrCreateGiteaOrg({
|
||||||
|
orgId: organization.id,
|
||||||
|
orgName: targetOrgName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
console.log(`Using single organization strategy: all repos will go to ${targetOrgName}`);
|
||||||
|
} else if (mirrorStrategy === "preserve") {
|
||||||
|
// For preserve strategy, create/use an org with the same name as GitHub
|
||||||
|
targetOrgName = organization.name;
|
||||||
|
giteaOrgId = await getOrCreateGiteaOrg({
|
||||||
|
orgId: organization.id,
|
||||||
|
orgName: targetOrgName,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// For flat-user strategy, we shouldn't create organizations at all
|
||||||
|
// Skip organization creation and let individual repos be handled by getGiteaRepoOwner
|
||||||
|
console.log(`Using flat-user strategy: repos will be placed under user account`);
|
||||||
|
targetOrgName = config.giteaConfig?.username || "";
|
||||||
|
}
|
||||||
|
|
||||||
//query the db with the org name and get the repos
|
//query the db with the org name and get the repos
|
||||||
const orgRepos = await db
|
const orgRepos = await db
|
||||||
@@ -629,60 +908,79 @@ export async function mirrorGitHubOrgToGitea({
|
|||||||
.where(eq(repositories.organization, organization.name));
|
.where(eq(repositories.organization, organization.name));
|
||||||
|
|
||||||
if (orgRepos.length === 0) {
|
if (orgRepos.length === 0) {
|
||||||
console.log(`No repositories found for organization ${organization.name}`);
|
console.log(
|
||||||
return;
|
`No repositories found for organization ${organization.name} - marking as successfully mirrored`
|
||||||
}
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Mirroring ${orgRepos.length} repositories for organization ${organization.name}`
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`Mirroring ${orgRepos.length} repositories for organization ${organization.name}`);
|
// Import the processWithRetry function
|
||||||
|
const { processWithRetry } = await import("@/lib/utils/concurrency");
|
||||||
|
|
||||||
// Import the processWithRetry function
|
// Process repositories in parallel with concurrency control
|
||||||
const { processWithRetry } = await import("@/lib/utils/concurrency");
|
await processWithRetry(
|
||||||
|
orgRepos,
|
||||||
|
async (repo) => {
|
||||||
|
// Prepare repository data
|
||||||
|
const repoData = {
|
||||||
|
...repo,
|
||||||
|
status: repo.status as RepoStatus,
|
||||||
|
visibility: repo.visibility as RepositoryVisibility,
|
||||||
|
lastMirrored: repo.lastMirrored ?? undefined,
|
||||||
|
errorMessage: repo.errorMessage ?? undefined,
|
||||||
|
organization: repo.organization ?? undefined,
|
||||||
|
forkedFrom: repo.forkedFrom ?? undefined,
|
||||||
|
mirroredLocation: repo.mirroredLocation || "",
|
||||||
|
};
|
||||||
|
|
||||||
// Process repositories in parallel with concurrency control
|
// Log the start of mirroring
|
||||||
await processWithRetry(
|
console.log(
|
||||||
orgRepos,
|
`Starting mirror for repository: ${repo.name} from GitHub org ${organization.name}`
|
||||||
async (repo) => {
|
);
|
||||||
// Prepare repository data
|
|
||||||
const repoData = {
|
|
||||||
...repo,
|
|
||||||
status: repo.status as RepoStatus,
|
|
||||||
visibility: repo.visibility as RepositoryVisibility,
|
|
||||||
lastMirrored: repo.lastMirrored ?? undefined,
|
|
||||||
errorMessage: repo.errorMessage ?? undefined,
|
|
||||||
organization: repo.organization ?? undefined,
|
|
||||||
forkedFrom: repo.forkedFrom ?? undefined,
|
|
||||||
mirroredLocation: repo.mirroredLocation || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log the start of mirroring
|
// Mirror the repository based on strategy
|
||||||
console.log(`Starting mirror for repository: ${repo.name} in organization ${organization.name}`);
|
if (mirrorStrategy === "flat-user") {
|
||||||
|
// For flat-user strategy, mirror directly to user account
|
||||||
// Mirror the repository
|
await mirrorGithubRepoToGitea({
|
||||||
await mirrorGitHubRepoToGiteaOrg({
|
octokit,
|
||||||
octokit,
|
repository: repoData,
|
||||||
config,
|
config,
|
||||||
repository: repoData,
|
});
|
||||||
giteaOrgId,
|
} else {
|
||||||
orgName: organization.name,
|
// For preserve and single-org strategies, use organization
|
||||||
});
|
await mirrorGitHubRepoToGiteaOrg({
|
||||||
|
octokit,
|
||||||
return repo;
|
config,
|
||||||
},
|
repository: repoData,
|
||||||
{
|
giteaOrgId: giteaOrgId!,
|
||||||
concurrencyLimit: 3, // Process 3 repositories at a time
|
orgName: targetOrgName,
|
||||||
maxRetries: 2,
|
});
|
||||||
retryDelay: 2000,
|
|
||||||
onProgress: (completed, total, result) => {
|
|
||||||
const percentComplete = Math.round((completed / total) * 100);
|
|
||||||
if (result) {
|
|
||||||
console.log(`Mirrored repository "${result.name}" in organization ${organization.name} (${completed}/${total}, ${percentComplete}%)`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return repo;
|
||||||
},
|
},
|
||||||
onRetry: (repo, error, attempt) => {
|
{
|
||||||
console.log(`Retrying repository ${repo.name} in organization ${organization.name} (attempt ${attempt}): ${error.message}`);
|
concurrencyLimit: 3, // Process 3 repositories at a time
|
||||||
|
maxRetries: 2,
|
||||||
|
retryDelay: 2000,
|
||||||
|
onProgress: (completed, total, result) => {
|
||||||
|
const percentComplete = Math.round((completed / total) * 100);
|
||||||
|
if (result) {
|
||||||
|
console.log(
|
||||||
|
`Mirrored repository "${result.name}" in organization ${organization.name} (${completed}/${total}, ${percentComplete}%)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRetry: (repo, error, attempt) => {
|
||||||
|
console.log(
|
||||||
|
`Retrying repository ${repo.name} in organization ${organization.name} (attempt ${attempt}): ${error.message}`
|
||||||
|
);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
console.log(`Organization ${organization.name} mirrored successfully`);
|
console.log(`Organization ${organization.name} mirrored successfully`);
|
||||||
|
|
||||||
@@ -703,7 +1001,10 @@ export async function mirrorGitHubOrgToGitea({
|
|||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
organizationName: organization.name,
|
organizationName: organization.name,
|
||||||
message: `Successfully mirrored organization: ${organization.name}`,
|
message: `Successfully mirrored organization: ${organization.name}`,
|
||||||
details: `Organization ${organization.name} was mirrored to Gitea.`,
|
details:
|
||||||
|
orgRepos.length === 0
|
||||||
|
? `Organization ${organization.name} was processed successfully (no repositories found).`
|
||||||
|
: `Organization ${organization.name} was mirrored to Gitea with ${orgRepos.length} repositories.`,
|
||||||
status: repoStatusEnum.parse("mirrored"),
|
status: repoStatusEnum.parse("mirrored"),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -780,25 +1081,27 @@ export const syncGiteaRepo = async ({
|
|||||||
status: repoStatusEnum.parse("syncing"),
|
status: repoStatusEnum.parse("syncing"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the expected owner based on current config
|
// Get the expected owner based on current config (with organization overrides)
|
||||||
const repoOwner = getGiteaRepoOwner({ config, repository });
|
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
||||||
|
|
||||||
// Check if repo exists at the expected location or alternate location
|
// Check if repo exists at the expected location or alternate location
|
||||||
const { present, actualOwner } = await checkRepoLocation({
|
const { present, actualOwner } = await checkRepoLocation({
|
||||||
config,
|
config,
|
||||||
repository,
|
repository,
|
||||||
expectedOwner: repoOwner
|
expectedOwner: repoOwner,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!present) {
|
if (!present) {
|
||||||
throw new Error(`Repository ${repository.name} not found in Gitea at any expected location`);
|
throw new Error(
|
||||||
|
`Repository ${repository.name} not found in Gitea at any expected location`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the actual owner where the repo was found
|
// Use the actual owner where the repo was found
|
||||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`;
|
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`;
|
||||||
|
|
||||||
const response = await httpPost(apiUrl, undefined, {
|
const response = await httpPost(apiUrl, undefined, {
|
||||||
"Authorization": `token ${config.giteaConfig.token}`,
|
Authorization: `token ${config.giteaConfig.token}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mark repo as "synced" in DB
|
// Mark repo as "synced" in DB
|
||||||
@@ -902,9 +1205,11 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Filter out pull requests
|
// Filter out pull requests
|
||||||
const filteredIssues = issues.filter(issue => !(issue as any).pull_request);
|
const filteredIssues = issues.filter((issue) => !(issue as any).pull_request);
|
||||||
|
|
||||||
console.log(`Mirroring ${filteredIssues.length} issues from ${repository.fullName}`);
|
console.log(
|
||||||
|
`Mirroring ${filteredIssues.length} issues from ${repository.fullName}`
|
||||||
|
);
|
||||||
|
|
||||||
if (filteredIssues.length === 0) {
|
if (filteredIssues.length === 0) {
|
||||||
console.log(`No issues to mirror for ${repository.fullName}`);
|
console.log(`No issues to mirror for ${repository.fullName}`);
|
||||||
@@ -915,7 +1220,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
const giteaLabelsRes = await httpGet(
|
const giteaLabelsRes = await httpGet(
|
||||||
`${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels`,
|
`${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels`,
|
||||||
{
|
{
|
||||||
"Authorization": `token ${config.giteaConfig.token}`,
|
Authorization: `token ${config.giteaConfig.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -945,10 +1250,12 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const created = await httpPost(
|
const created = await httpPost(
|
||||||
`${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels`,
|
`${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${
|
||||||
|
repository.name
|
||||||
|
}/labels`,
|
||||||
{ name, color: "#ededed" }, // Default color
|
{ name, color: "#ededed" }, // Default color
|
||||||
{
|
{
|
||||||
"Authorization": `token ${config.giteaConfig!.token}`,
|
Authorization: `token ${config.giteaConfig!.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -980,10 +1287,12 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
|
|
||||||
// Create the issue in Gitea
|
// Create the issue in Gitea
|
||||||
const createdIssue = await httpPost(
|
const createdIssue = await httpPost(
|
||||||
`${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${repository.name}/issues`,
|
`${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${
|
||||||
|
repository.name
|
||||||
|
}/issues`,
|
||||||
issuePayload,
|
issuePayload,
|
||||||
{
|
{
|
||||||
"Authorization": `token ${config.giteaConfig!.token}`,
|
Authorization: `token ${config.giteaConfig!.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1005,12 +1314,14 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
comments,
|
comments,
|
||||||
async (comment) => {
|
async (comment) => {
|
||||||
await httpPost(
|
await httpPost(
|
||||||
`${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${repository.name}/issues/${createdIssue.data.number}/comments`,
|
`${config.giteaConfig!.url}/api/v1/repos/${repoOrigin}/${
|
||||||
|
repository.name
|
||||||
|
}/issues/${createdIssue.data.number}/comments`,
|
||||||
{
|
{
|
||||||
body: `@${comment.user?.login} commented on GitHub:\n\n${comment.body}`,
|
body: `@${comment.user?.login} commented on GitHub:\n\n${comment.body}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Authorization": `token ${config.giteaConfig!.token}`,
|
Authorization: `token ${config.giteaConfig!.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return comment;
|
return comment;
|
||||||
@@ -1020,8 +1331,10 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
maxRetries: 2,
|
maxRetries: 2,
|
||||||
retryDelay: 1000,
|
retryDelay: 1000,
|
||||||
onRetry: (_comment, error, attempt) => {
|
onRetry: (_comment, error, attempt) => {
|
||||||
console.log(`Retrying comment (attempt ${attempt}): ${error.message}`);
|
console.log(
|
||||||
}
|
`Retrying comment (attempt ${attempt}): ${error.message}`
|
||||||
|
);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1035,14 +1348,69 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
onProgress: (completed, total, result) => {
|
onProgress: (completed, total, result) => {
|
||||||
const percentComplete = Math.round((completed / total) * 100);
|
const percentComplete = Math.round((completed / total) * 100);
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log(`Mirrored issue "${result.title}" (${completed}/${total}, ${percentComplete}%)`);
|
console.log(
|
||||||
|
`Mirrored issue "${result.title}" (${completed}/${total}, ${percentComplete}%)`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRetry: (issue, error, attempt) => {
|
onRetry: (issue, error, attempt) => {
|
||||||
console.log(`Retrying issue "${issue.title}" (attempt ${attempt}): ${error.message}`);
|
console.log(
|
||||||
}
|
`Retrying issue "${issue.title}" (attempt ${attempt}): ${error.message}`
|
||||||
|
);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Completed mirroring ${filteredIssues.length} issues for ${repository.fullName}`);
|
console.log(
|
||||||
|
`Completed mirroring ${filteredIssues.length} issues for ${repository.fullName}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function mirrorGitHubReleasesToGitea({
|
||||||
|
octokit,
|
||||||
|
repository,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
octokit: Octokit;
|
||||||
|
repository: Repository;
|
||||||
|
config: Partial<Config>;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
!config.giteaConfig?.username ||
|
||||||
|
!config.giteaConfig?.token ||
|
||||||
|
!config.giteaConfig?.url
|
||||||
|
) {
|
||||||
|
throw new Error("Gitea config is incomplete for mirroring releases.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoOwner = await getGiteaRepoOwnerAsync({
|
||||||
|
config,
|
||||||
|
repository,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, token } = config.giteaConfig;
|
||||||
|
|
||||||
|
const releases = await octokit.rest.repos.listReleases({
|
||||||
|
owner: repository.owner,
|
||||||
|
repo: repository.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const release of releases.data) {
|
||||||
|
await httpPost(
|
||||||
|
`${url}/api/v1/repos/${repoOwner}/${repository.name}/releases`,
|
||||||
|
{
|
||||||
|
tag_name: release.tag_name,
|
||||||
|
target: release.target_commitish,
|
||||||
|
title: release.name || release.tag_name,
|
||||||
|
note: release.body || "",
|
||||||
|
draft: release.draft,
|
||||||
|
prerelease: release.prerelease,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Mirrored ${releases.data.length} GitHub releases to Gitea`);
|
||||||
|
}
|
||||||
@@ -216,3 +216,76 @@ export const jsonResponse = ({
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Securely handles errors for API responses by sanitizing error messages
|
||||||
|
* and preventing sensitive information exposure while maintaining proper logging
|
||||||
|
*/
|
||||||
|
export function createSecureErrorResponse(
|
||||||
|
error: unknown,
|
||||||
|
context: string,
|
||||||
|
status: number = 500
|
||||||
|
): Response {
|
||||||
|
// Log the full error details server-side for debugging
|
||||||
|
console.error(`Error in ${context}:`, error);
|
||||||
|
|
||||||
|
// Log additional error details if it's an Error object
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`Error name: ${error.name}`);
|
||||||
|
console.error(`Error message: ${error.message}`);
|
||||||
|
if (error.stack) {
|
||||||
|
console.error(`Error stack: ${error.stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine safe error message for client
|
||||||
|
let clientMessage = "An internal server error occurred";
|
||||||
|
|
||||||
|
// Only expose specific safe error types to clients
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// Safe error patterns that can be exposed (add more as needed)
|
||||||
|
const safeErrorPatterns = [
|
||||||
|
/missing required field/i,
|
||||||
|
/invalid.*format/i,
|
||||||
|
/not found/i,
|
||||||
|
/unauthorized/i,
|
||||||
|
/forbidden/i,
|
||||||
|
/bad request/i,
|
||||||
|
/validation.*failed/i,
|
||||||
|
/user id is required/i,
|
||||||
|
/no repositories found/i,
|
||||||
|
/config missing/i,
|
||||||
|
/invalid userid/i,
|
||||||
|
/no users found/i,
|
||||||
|
/missing userid/i,
|
||||||
|
/github token is required/i,
|
||||||
|
/invalid github token/i,
|
||||||
|
/invalid gitea token/i,
|
||||||
|
/username and password are required/i,
|
||||||
|
/invalid username or password/i,
|
||||||
|
/organization already exists/i,
|
||||||
|
/no configuration found/i,
|
||||||
|
/github token is missing/i,
|
||||||
|
/use post method/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const isSafeError = safeErrorPatterns.some(pattern =>
|
||||||
|
pattern.test(error.message)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSafeError) {
|
||||||
|
clientMessage = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: clientMessage,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
136
src/lib/utils/config-mapper.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Maps between UI config structure and database schema structure
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
GitHubConfig,
|
||||||
|
GiteaConfig,
|
||||||
|
MirrorOptions,
|
||||||
|
AdvancedOptions,
|
||||||
|
SaveConfigApiRequest
|
||||||
|
} from "@/types/config";
|
||||||
|
|
||||||
|
interface DbGitHubConfig {
|
||||||
|
username: string;
|
||||||
|
token?: string;
|
||||||
|
skipForks: boolean;
|
||||||
|
privateRepositories: boolean;
|
||||||
|
mirrorIssues: boolean;
|
||||||
|
mirrorWiki: boolean;
|
||||||
|
mirrorStarred: boolean;
|
||||||
|
useSpecificUser: boolean;
|
||||||
|
singleRepo?: string;
|
||||||
|
includeOrgs: string[];
|
||||||
|
excludeOrgs: string[];
|
||||||
|
mirrorPublicOrgs: boolean;
|
||||||
|
publicOrgs: string[];
|
||||||
|
skipStarredIssues: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DbGiteaConfig {
|
||||||
|
username: string;
|
||||||
|
url: string;
|
||||||
|
token: string;
|
||||||
|
organization?: string;
|
||||||
|
visibility: "public" | "private" | "limited";
|
||||||
|
starredReposOrg: string;
|
||||||
|
preserveOrgStructure: boolean;
|
||||||
|
mirrorStrategy?: "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
personalReposOrg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps UI config structure to database schema structure
|
||||||
|
*/
|
||||||
|
export function mapUiToDbConfig(
|
||||||
|
githubConfig: GitHubConfig,
|
||||||
|
giteaConfig: GiteaConfig,
|
||||||
|
mirrorOptions: MirrorOptions,
|
||||||
|
advancedOptions: AdvancedOptions
|
||||||
|
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
|
||||||
|
// Map GitHub config with fields from mirrorOptions and advancedOptions
|
||||||
|
const dbGithubConfig: DbGitHubConfig = {
|
||||||
|
username: githubConfig.username,
|
||||||
|
token: githubConfig.token,
|
||||||
|
privateRepositories: githubConfig.privateRepositories,
|
||||||
|
mirrorStarred: githubConfig.mirrorStarred,
|
||||||
|
|
||||||
|
// From mirrorOptions
|
||||||
|
mirrorIssues: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||||
|
mirrorWiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||||
|
|
||||||
|
// From advancedOptions
|
||||||
|
skipForks: advancedOptions.skipForks,
|
||||||
|
skipStarredIssues: advancedOptions.skipStarredIssues,
|
||||||
|
|
||||||
|
// Default values for fields not in UI
|
||||||
|
useSpecificUser: false,
|
||||||
|
includeOrgs: [],
|
||||||
|
excludeOrgs: [],
|
||||||
|
mirrorPublicOrgs: false,
|
||||||
|
publicOrgs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gitea config remains mostly the same
|
||||||
|
const dbGiteaConfig: DbGiteaConfig = {
|
||||||
|
...giteaConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
githubConfig: dbGithubConfig,
|
||||||
|
giteaConfig: dbGiteaConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps database schema structure to UI config structure
|
||||||
|
*/
|
||||||
|
export function mapDbToUiConfig(dbConfig: any): {
|
||||||
|
githubConfig: GitHubConfig;
|
||||||
|
giteaConfig: GiteaConfig;
|
||||||
|
mirrorOptions: MirrorOptions;
|
||||||
|
advancedOptions: AdvancedOptions;
|
||||||
|
} {
|
||||||
|
const githubConfig: GitHubConfig = {
|
||||||
|
username: dbConfig.githubConfig?.username || "",
|
||||||
|
token: dbConfig.githubConfig?.token || "",
|
||||||
|
privateRepositories: dbConfig.githubConfig?.privateRepositories || false,
|
||||||
|
mirrorStarred: dbConfig.githubConfig?.mirrorStarred || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const giteaConfig: GiteaConfig = {
|
||||||
|
url: dbConfig.giteaConfig?.url || "",
|
||||||
|
username: dbConfig.giteaConfig?.username || "",
|
||||||
|
token: dbConfig.giteaConfig?.token || "",
|
||||||
|
organization: dbConfig.giteaConfig?.organization || "github-mirrors",
|
||||||
|
visibility: dbConfig.giteaConfig?.visibility || "public",
|
||||||
|
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github",
|
||||||
|
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false,
|
||||||
|
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy,
|
||||||
|
personalReposOrg: dbConfig.giteaConfig?.personalReposOrg,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mirrorOptions: MirrorOptions = {
|
||||||
|
mirrorReleases: false, // Not stored in DB yet
|
||||||
|
mirrorMetadata: dbConfig.githubConfig?.mirrorIssues || dbConfig.githubConfig?.mirrorWiki || false,
|
||||||
|
metadataComponents: {
|
||||||
|
issues: dbConfig.githubConfig?.mirrorIssues || false,
|
||||||
|
pullRequests: false, // Not stored in DB yet
|
||||||
|
labels: false, // Not stored in DB yet
|
||||||
|
milestones: false, // Not stored in DB yet
|
||||||
|
wiki: dbConfig.githubConfig?.mirrorWiki || false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const advancedOptions: AdvancedOptions = {
|
||||||
|
skipForks: dbConfig.githubConfig?.skipForks || false,
|
||||||
|
skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
githubConfig,
|
||||||
|
giteaConfig,
|
||||||
|
mirrorOptions,
|
||||||
|
advancedOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
37
src/pages/404.astro
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import '../styles/global.css';
|
||||||
|
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||||
|
import { NotFound } from '@/components/NotFound';
|
||||||
|
|
||||||
|
const generator = Astro.generator;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="generator" content={generator} />
|
||||||
|
<title>Page Not Found - Gitea Mirror</title>
|
||||||
|
<ThemeScript />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<NotFound client:load />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Floating animation for 404 text */
|
||||||
|
:global(.animate-float) {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { db, mirrorJobs, events } from "@/lib/db";
|
import { db, mirrorJobs, events } from "@/lib/db";
|
||||||
import { eq, count } from "drizzle-orm";
|
import { eq, count } from "drizzle-orm";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -87,29 +88,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error cleaning up activities:", error);
|
return createSecureErrorResponse(error, "activities cleanup", 500);
|
||||||
|
|
||||||
// Provide more specific error messages
|
|
||||||
let errorMessage = "An unknown error occurred.";
|
|
||||||
if (error instanceof Error) {
|
|
||||||
errorMessage = error.message;
|
|
||||||
|
|
||||||
// Check for common database errors
|
|
||||||
if (error.message.includes("FOREIGN KEY constraint failed")) {
|
|
||||||
errorMessage = "Cannot delete activities due to database constraints. Some jobs may still be referenced by other records.";
|
|
||||||
} else if (error.message.includes("database is locked")) {
|
|
||||||
errorMessage = "Database is currently locked. Please try again in a moment.";
|
|
||||||
} else if (error.message.includes("no such table")) {
|
|
||||||
errorMessage = "Database tables are missing. Please check your database setup.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: errorMessage,
|
|
||||||
}),
|
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { db, mirrorJobs, configs } from "@/lib/db";
|
import { db, mirrorJobs, configs } from "@/lib/db";
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
import type { MirrorJob } from "@/lib/db/schema";
|
import type { MirrorJob } from "@/lib/db/schema";
|
||||||
import { repoStatusEnum } from "@/types/Repository";
|
import { repoStatusEnum } from "@/types/Repository";
|
||||||
|
|
||||||
@@ -45,14 +46,6 @@ export const GET: APIRoute = async ({ url }) => {
|
|||||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching mirror job activities:", error);
|
return createSecureErrorResponse(error, "activities fetch", 500);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
error instanceof Error ? error.message : "An unknown error occurred.",
|
|
||||||
}),
|
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { runAutomaticCleanup } from '@/lib/cleanup-service';
|
import { runAutomaticCleanup } from '@/lib/cleanup-service';
|
||||||
|
import { createSecureErrorResponse } from '@/lib/utils';
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -38,21 +39,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in manual cleanup trigger:', error);
|
return createSecureErrorResponse(error, "cleanup trigger", 500);
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
message: 'Failed to run automatic cleanup',
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,20 @@ import { db, configs, users } from "@/lib/db";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { calculateCleanupInterval } from "@/lib/cleanup-service";
|
import { calculateCleanupInterval } from "@/lib/cleanup-service";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
import { mapUiToDbConfig, mapDbToUiConfig } from "@/lib/utils/config-mapper";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig } = body;
|
const { userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, advancedOptions } = body;
|
||||||
|
|
||||||
if (!userId || !githubConfig || !giteaConfig || !scheduleConfig || !cleanupConfig) {
|
if (!userId || !githubConfig || !giteaConfig || !scheduleConfig || !cleanupConfig || !mirrorOptions || !advancedOptions) {
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: false,
|
success: false,
|
||||||
message:
|
message:
|
||||||
"userId, githubConfig, giteaConfig, scheduleConfig, and cleanupConfig are required.",
|
"userId, githubConfig, giteaConfig, scheduleConfig, cleanupConfig, mirrorOptions, and advancedOptions are required.",
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
@@ -32,6 +34,14 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
const existingConfig = existingConfigResult[0];
|
const existingConfig = existingConfigResult[0];
|
||||||
|
|
||||||
|
// Map UI structure to database schema structure first
|
||||||
|
const { githubConfig: mappedGithubConfig, giteaConfig: mappedGiteaConfig } = mapUiToDbConfig(
|
||||||
|
githubConfig,
|
||||||
|
giteaConfig,
|
||||||
|
mirrorOptions,
|
||||||
|
advancedOptions
|
||||||
|
);
|
||||||
|
|
||||||
// Preserve tokens if fields are empty
|
// Preserve tokens if fields are empty
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
try {
|
try {
|
||||||
@@ -45,12 +55,12 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
? JSON.parse(existingConfig.giteaConfig)
|
? JSON.parse(existingConfig.giteaConfig)
|
||||||
: existingConfig.giteaConfig;
|
: existingConfig.giteaConfig;
|
||||||
|
|
||||||
if (!githubConfig.token && existingGithub.token) {
|
if (!mappedGithubConfig.token && existingGithub.token) {
|
||||||
githubConfig.token = existingGithub.token;
|
mappedGithubConfig.token = existingGithub.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!giteaConfig.token && existingGitea.token) {
|
if (!mappedGiteaConfig.token && existingGitea.token) {
|
||||||
giteaConfig.token = existingGitea.token;
|
mappedGiteaConfig.token = existingGitea.token;
|
||||||
}
|
}
|
||||||
} catch (tokenError) {
|
} catch (tokenError) {
|
||||||
console.error("Failed to preserve tokens:", tokenError);
|
console.error("Failed to preserve tokens:", tokenError);
|
||||||
@@ -119,8 +129,8 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
await db
|
await db
|
||||||
.update(configs)
|
.update(configs)
|
||||||
.set({
|
.set({
|
||||||
githubConfig,
|
githubConfig: mappedGithubConfig,
|
||||||
giteaConfig,
|
giteaConfig: mappedGiteaConfig,
|
||||||
scheduleConfig: processedScheduleConfig,
|
scheduleConfig: processedScheduleConfig,
|
||||||
cleanupConfig: processedCleanupConfig,
|
cleanupConfig: processedCleanupConfig,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -167,8 +177,8 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
userId,
|
userId,
|
||||||
name: "Default Configuration",
|
name: "Default Configuration",
|
||||||
isActive: true,
|
isActive: true,
|
||||||
githubConfig,
|
githubConfig: mappedGithubConfig,
|
||||||
giteaConfig,
|
giteaConfig: mappedGiteaConfig,
|
||||||
include: [],
|
include: [],
|
||||||
exclude: [],
|
exclude: [],
|
||||||
scheduleConfig: processedScheduleConfig,
|
scheduleConfig: processedScheduleConfig,
|
||||||
@@ -189,19 +199,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving configuration:", error);
|
return createSecureErrorResponse(error, "config save", 500);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
message:
|
|
||||||
"Error saving configuration: " +
|
|
||||||
(error instanceof Error ? error.message : "Unknown error"),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,32 +223,40 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (config.length === 0) {
|
if (config.length === 0) {
|
||||||
// Return a default empty configuration instead of a 404 error
|
// Return a default empty configuration with UI structure
|
||||||
|
const defaultDbConfig = {
|
||||||
|
githubConfig: {
|
||||||
|
username: "",
|
||||||
|
token: "",
|
||||||
|
skipForks: false,
|
||||||
|
privateRepositories: false,
|
||||||
|
mirrorIssues: false,
|
||||||
|
mirrorWiki: false,
|
||||||
|
mirrorStarred: false,
|
||||||
|
useSpecificUser: false,
|
||||||
|
preserveOrgStructure: false,
|
||||||
|
skipStarredIssues: false,
|
||||||
|
},
|
||||||
|
giteaConfig: {
|
||||||
|
url: "",
|
||||||
|
token: "",
|
||||||
|
username: "",
|
||||||
|
organization: "github-mirrors",
|
||||||
|
visibility: "public",
|
||||||
|
starredReposOrg: "github",
|
||||||
|
preserveOrgStructure: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uiConfig = mapDbToUiConfig(defaultDbConfig);
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
id: null,
|
id: null,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
name: "Default Configuration",
|
name: "Default Configuration",
|
||||||
isActive: true,
|
isActive: true,
|
||||||
githubConfig: {
|
...uiConfig,
|
||||||
username: "",
|
|
||||||
token: "",
|
|
||||||
skipForks: false,
|
|
||||||
privateRepositories: false,
|
|
||||||
mirrorIssues: false,
|
|
||||||
mirrorStarred: true,
|
|
||||||
useSpecificUser: false,
|
|
||||||
preserveOrgStructure: true,
|
|
||||||
skipStarredIssues: false,
|
|
||||||
},
|
|
||||||
giteaConfig: {
|
|
||||||
url: "",
|
|
||||||
token: "",
|
|
||||||
username: "",
|
|
||||||
organization: "github-mirrors",
|
|
||||||
visibility: "public",
|
|
||||||
starredReposOrg: "github",
|
|
||||||
},
|
|
||||||
scheduleConfig: {
|
scheduleConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
interval: 3600,
|
interval: 3600,
|
||||||
@@ -271,21 +277,18 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify(config[0]), {
|
// Map database structure to UI structure
|
||||||
|
const dbConfig = config[0];
|
||||||
|
const uiConfig = mapDbToUiConfig(dbConfig);
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
...dbConfig,
|
||||||
|
...uiConfig,
|
||||||
|
}), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching configuration:", error);
|
return createSecureErrorResponse(error, "config fetch", 500);
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: error instanceof Error ? error.message : "Something went wrong",
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { db, repositories, organizations, mirrorJobs, configs } from "@/lib/db";
|
import { db, repositories, organizations, mirrorJobs, configs } from "@/lib/db";
|
||||||
import { eq, count, and, sql, or } from "drizzle-orm";
|
import { eq, count, and, sql, or } from "drizzle-orm";
|
||||||
import { jsonResponse } from "@/lib/utils";
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||||
import type { DashboardApiResponse } from "@/types/dashboard";
|
import type { DashboardApiResponse } from "@/types/dashboard";
|
||||||
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
||||||
import { membershipRoleEnum } from "@/types/organizations";
|
import { membershipRoleEnum } from "@/types/organizations";
|
||||||
@@ -108,15 +108,6 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
return jsonResponse({ data: successResponse });
|
return jsonResponse({ data: successResponse });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading dashboard for user:", userId, error);
|
return createSecureErrorResponse(error, "dashboard data fetch", 500);
|
||||||
|
|
||||||
return jsonResponse({
|
|
||||||
data: {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "Internal server error",
|
|
||||||
message: "Failed to fetch dashboard data",
|
|
||||||
},
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { httpGet, HttpError } from '@/lib/http-client';
|
import { httpGet, HttpError } from '@/lib/http-client';
|
||||||
|
import { createSecureErrorResponse } from '@/lib/utils';
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -115,17 +116,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generic error response
|
// Generic error response
|
||||||
return new Response(
|
return createSecureErrorResponse(error, "Gitea connection test", 500);
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
message: `Gitea connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { organizations } from "@/lib/db";
|
import { organizations, repositories, configs } from "@/lib/db";
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql, and, count } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
membershipRoleEnum,
|
membershipRoleEnum,
|
||||||
type OrganizationsApiResponse,
|
type OrganizationsApiResponse,
|
||||||
} from "@/types/organizations";
|
} from "@/types/organizations";
|
||||||
import type { Organization } from "@/lib/db/schema";
|
import type { Organization } from "@/lib/db/schema";
|
||||||
import { repoStatusEnum } from "@/types/Repository";
|
import { repoStatusEnum } from "@/types/Repository";
|
||||||
import { jsonResponse } from "@/lib/utils";
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
@@ -25,36 +25,118 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Fetch the user's active configuration to respect filtering settings
|
||||||
|
const [config] = await db
|
||||||
|
.select()
|
||||||
|
.from(configs)
|
||||||
|
.where(and(eq(configs.userId, userId), eq(configs.isActive, true)));
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return jsonResponse({
|
||||||
|
data: {
|
||||||
|
success: false,
|
||||||
|
error: "No active configuration found for this user",
|
||||||
|
},
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const githubConfig = config.githubConfig as {
|
||||||
|
mirrorStarred: boolean;
|
||||||
|
skipForks: boolean;
|
||||||
|
privateRepositories: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const rawOrgs = await db
|
const rawOrgs = await db
|
||||||
.select()
|
.select()
|
||||||
.from(organizations)
|
.from(organizations)
|
||||||
.where(eq(organizations.userId, userId))
|
.where(eq(organizations.userId, userId))
|
||||||
.orderBy(sql`name COLLATE NOCASE`);
|
.orderBy(sql`name COLLATE NOCASE`);
|
||||||
|
|
||||||
const orgsWithIds: Organization[] = rawOrgs.map((org) => ({
|
// Calculate repository breakdowns for each organization
|
||||||
...org,
|
const orgsWithBreakdown = await Promise.all(
|
||||||
status: repoStatusEnum.parse(org.status),
|
rawOrgs.map(async (org) => {
|
||||||
membershipRole: membershipRoleEnum.parse(org.membershipRole),
|
// Build base conditions for this organization (without private/fork filters)
|
||||||
lastMirrored: org.lastMirrored ?? undefined,
|
const baseConditions = [
|
||||||
errorMessage: org.errorMessage ?? undefined,
|
eq(repositories.userId, userId),
|
||||||
}));
|
eq(repositories.organization, org.name)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!githubConfig.mirrorStarred) {
|
||||||
|
baseConditions.push(eq(repositories.isStarred, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count with all user config filters applied
|
||||||
|
const totalConditions = [...baseConditions];
|
||||||
|
if (githubConfig.skipForks) {
|
||||||
|
totalConditions.push(eq(repositories.isForked, false));
|
||||||
|
}
|
||||||
|
if (!githubConfig.privateRepositories) {
|
||||||
|
totalConditions.push(eq(repositories.isPrivate, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [totalCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(repositories)
|
||||||
|
.where(and(...totalConditions));
|
||||||
|
|
||||||
|
// Get public count
|
||||||
|
const publicConditions = [...baseConditions, eq(repositories.isPrivate, false)];
|
||||||
|
if (githubConfig.skipForks) {
|
||||||
|
publicConditions.push(eq(repositories.isForked, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [publicCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(repositories)
|
||||||
|
.where(and(...publicConditions));
|
||||||
|
|
||||||
|
// Get private count (only if private repos are enabled in config)
|
||||||
|
const [privateCount] = githubConfig.privateRepositories ? await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(repositories)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
...baseConditions,
|
||||||
|
eq(repositories.isPrivate, true),
|
||||||
|
...(githubConfig.skipForks ? [eq(repositories.isForked, false)] : [])
|
||||||
|
)
|
||||||
|
) : [{ count: 0 }];
|
||||||
|
|
||||||
|
// Get fork count (only if forks are enabled in config)
|
||||||
|
const [forkCount] = !githubConfig.skipForks ? await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(repositories)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
...baseConditions,
|
||||||
|
eq(repositories.isForked, true),
|
||||||
|
...(!githubConfig.privateRepositories ? [eq(repositories.isPrivate, false)] : [])
|
||||||
|
)
|
||||||
|
) : [{ count: 0 }];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...org,
|
||||||
|
status: repoStatusEnum.parse(org.status),
|
||||||
|
membershipRole: membershipRoleEnum.parse(org.membershipRole),
|
||||||
|
lastMirrored: org.lastMirrored ?? undefined,
|
||||||
|
errorMessage: org.errorMessage ?? undefined,
|
||||||
|
repositoryCount: totalCount.count,
|
||||||
|
publicRepositoryCount: publicCount.count,
|
||||||
|
privateRepositoryCount: privateCount.count,
|
||||||
|
forkRepositoryCount: forkCount.count,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const resPayload: OrganizationsApiResponse = {
|
const resPayload: OrganizationsApiResponse = {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Organizations fetched successfully",
|
message: "Organizations fetched successfully",
|
||||||
organizations: orgsWithIds,
|
organizations: orgsWithBreakdown,
|
||||||
};
|
};
|
||||||
|
|
||||||
return jsonResponse({ data: resPayload, status: 200 });
|
return jsonResponse({ data: resPayload, status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching organizations:", error);
|
return createSecureErrorResponse(error, "organizations fetch", 500);
|
||||||
|
|
||||||
return jsonResponse({
|
|
||||||
data: {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "Something went wrong",
|
|
||||||
},
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
repoStatusEnum,
|
repoStatusEnum,
|
||||||
type RepositoryApiResponse,
|
type RepositoryApiResponse,
|
||||||
} from "@/types/Repository";
|
} from "@/types/Repository";
|
||||||
import { jsonResponse } from "@/lib/utils";
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ request }) => {
|
export const GET: APIRoute = async ({ request }) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
@@ -82,15 +82,6 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching repositories:", error);
|
return createSecureErrorResponse(error, "repositories fetch", 500);
|
||||||
|
|
||||||
return jsonResponse({
|
|
||||||
data: {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : "Something went wrong",
|
|
||||||
message: "An error occurred while fetching repositories.",
|
|
||||||
},
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ describe("GitHub Test Connection API", () => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const request = new Request("http://localhost/api/github/test-connection", {
|
const request = new Request("http://localhost/api/github/test-connection", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -121,13 +121,15 @@ describe("GitHub Test Connection API", () => {
|
|||||||
token: "invalid-token"
|
token: "invalid-token"
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await POST({ request } as any);
|
const response = await POST({ request } as any);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
expect(data.success).toBe(false);
|
// The createSecureErrorResponse function returns an error field, not success
|
||||||
expect(data.message).toContain("Bad credentials");
|
// It sanitizes error messages for security, so we expect the generic message
|
||||||
|
expect(data.error).toBeDefined();
|
||||||
|
expect(data.error).toBe("An internal server error occurred");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -83,19 +84,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generic error response
|
// Generic error response
|
||||||
return new Response(
|
return createSecureErrorResponse(error, "GitHub connection test", 500);
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
message: `GitHub connection test failed: ${
|
|
||||||
error instanceof Error ? error.message : "Unknown error"
|
|
||||||
}`,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 500,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { jsonResponse } from "@/lib/utils";
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { ENV } from "@/lib/config";
|
import { ENV } from "@/lib/config";
|
||||||
import { getRecoveryStatus, hasJobsNeedingRecovery } from "@/lib/recovery";
|
import { getRecoveryStatus, hasJobsNeedingRecovery } from "@/lib/recovery";
|
||||||
@@ -58,7 +58,7 @@ export const GET: APIRoute = async () => {
|
|||||||
latestVersion: latestVersion,
|
latestVersion: latestVersion,
|
||||||
updateAvailable: latestVersion !== "unknown" &&
|
updateAvailable: latestVersion !== "unknown" &&
|
||||||
currentVersion !== "unknown" &&
|
currentVersion !== "unknown" &&
|
||||||
latestVersion !== currentVersion,
|
compareVersions(currentVersion, latestVersion) < 0,
|
||||||
database: dbStatus,
|
database: dbStatus,
|
||||||
recovery: recoveryStatus,
|
recovery: recoveryStatus,
|
||||||
system: systemInfo,
|
system: systemInfo,
|
||||||
@@ -69,19 +69,7 @@ export const GET: APIRoute = async () => {
|
|||||||
status: 200,
|
status: 200,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Health check failed:", error);
|
return createSecureErrorResponse(error, "health check", 503);
|
||||||
|
|
||||||
return jsonResponse({
|
|
||||||
data: {
|
|
||||||
status: "error",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
error: error instanceof Error ? error.message : "Unknown error",
|
|
||||||
version: process.env.npm_package_version || "unknown",
|
|
||||||
latestVersion: "unknown",
|
|
||||||
updateAvailable: false,
|
|
||||||
},
|
|
||||||
status: 503, // Service Unavailable
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,6 +174,28 @@ function formatBytes(bytes: number): string {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare semantic versions
|
||||||
|
* Returns:
|
||||||
|
* -1 if v1 < v2
|
||||||
|
* 0 if v1 = v2
|
||||||
|
* 1 if v1 > v2
|
||||||
|
*/
|
||||||
|
function compareVersions(v1: string, v2: string): number {
|
||||||
|
const parts1 = v1.split('.').map(Number);
|
||||||
|
const parts2 = v2.split('.').map(Number);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||||
|
const part1 = parts1[i] || 0;
|
||||||
|
const part2 = parts2[i] || 0;
|
||||||
|
|
||||||
|
if (part1 < part2) return -1;
|
||||||
|
if (part1 > part2) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for the latest version from GitHub releases
|
* Check for the latest version from GitHub releases
|
||||||
*/
|
*/
|
||||||
@@ -198,7 +208,7 @@ async function checkLatestVersion(): Promise<string> {
|
|||||||
try {
|
try {
|
||||||
// Fetch the latest release from GitHub
|
// Fetch the latest release from GitHub
|
||||||
const response = await httpGet(
|
const response = await httpGet(
|
||||||
'https://api.github.com/repos/arunavo4/gitea-mirror/releases/latest',
|
'https://api.github.com/repos/RayLabsHQ/gitea-mirror/releases/latest',
|
||||||
{ 'Accept': 'application/vnd.github.v3+json' }
|
{ 'Accept': 'application/vnd.github.v3+json' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createGitHubClient } from "@/lib/github";
|
|||||||
import { mirrorGitHubOrgToGitea } from "@/lib/gitea";
|
import { mirrorGitHubOrgToGitea } from "@/lib/gitea";
|
||||||
import { repoStatusEnum } from "@/types/Repository";
|
import { repoStatusEnum } from "@/types/Repository";
|
||||||
import { type MembershipRole } from "@/types/organizations";
|
import { type MembershipRole } from "@/types/organizations";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
@@ -149,13 +150,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in mirroring organization:", error);
|
return createSecureErrorResponse(error, "mirror organization", 500);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error:
|
|
||||||
error instanceof Error ? error.message : "An unknown error occurred.",
|
|
||||||
}),
|
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +1,109 @@
|
|||||||
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
||||||
|
import type { MirrorRepoRequest } from "@/types/mirror";
|
||||||
|
import { POST } from "./mirror-repo";
|
||||||
|
|
||||||
// Create a mock POST function
|
// Mock the database module
|
||||||
const mockPOST = mock(async ({ request }) => {
|
const mockDb = {
|
||||||
const body = await request.json();
|
select: mock(() => ({
|
||||||
|
from: mock((table: any) => ({
|
||||||
// Check for missing userId or repositoryIds
|
where: mock(() => {
|
||||||
if (!body.userId || !body.repositoryIds) {
|
// Return config for configs table
|
||||||
return new Response(
|
if (table === mockConfigs) {
|
||||||
JSON.stringify({
|
return {
|
||||||
error: "Missing userId or repositoryIds."
|
limit: mock(() => Promise.resolve([{
|
||||||
}),
|
id: "config-id",
|
||||||
{ status: 400 }
|
userId: "user-id",
|
||||||
);
|
githubConfig: {
|
||||||
}
|
token: "github-token",
|
||||||
|
preserveOrgStructure: false,
|
||||||
// Success case
|
mirrorIssues: false
|
||||||
return new Response(
|
},
|
||||||
JSON.stringify({
|
giteaConfig: {
|
||||||
success: true,
|
url: "https://gitea.example.com",
|
||||||
message: "Repository mirroring started",
|
token: "gitea-token",
|
||||||
batchId: "test-batch-id"
|
username: "giteauser"
|
||||||
}),
|
}
|
||||||
{ status: 200 }
|
}]))
|
||||||
);
|
};
|
||||||
});
|
}
|
||||||
|
// Return repositories for repositories table
|
||||||
// Create a mock module
|
return Promise.resolve([
|
||||||
const mockModule = {
|
{
|
||||||
POST: mockPOST
|
id: "repo-id-1",
|
||||||
|
name: "test-repo-1",
|
||||||
|
visibility: "public",
|
||||||
|
status: "pending",
|
||||||
|
organization: null,
|
||||||
|
lastMirrored: null,
|
||||||
|
errorMessage: null,
|
||||||
|
forkedFrom: null,
|
||||||
|
mirroredLocation: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "repo-id-2",
|
||||||
|
name: "test-repo-2",
|
||||||
|
visibility: "public",
|
||||||
|
status: "pending",
|
||||||
|
organization: null,
|
||||||
|
lastMirrored: null,
|
||||||
|
errorMessage: null,
|
||||||
|
forkedFrom: null,
|
||||||
|
mirroredLocation: ""
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockConfigs = {};
|
||||||
|
const mockRepositories = {};
|
||||||
|
|
||||||
|
mock.module("@/lib/db", () => ({
|
||||||
|
db: mockDb,
|
||||||
|
configs: mockConfigs,
|
||||||
|
repositories: mockRepositories
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the gitea module
|
||||||
|
const mockMirrorGithubRepoToGitea = mock(() => Promise.resolve());
|
||||||
|
const mockMirrorGitHubOrgRepoToGiteaOrg = mock(() => Promise.resolve());
|
||||||
|
|
||||||
|
mock.module("@/lib/gitea", () => ({
|
||||||
|
mirrorGithubRepoToGitea: mockMirrorGithubRepoToGitea,
|
||||||
|
mirrorGitHubOrgRepoToGiteaOrg: mockMirrorGitHubOrgRepoToGiteaOrg
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the github module
|
||||||
|
const mockCreateGitHubClient = mock(() => ({}));
|
||||||
|
|
||||||
|
mock.module("@/lib/github", () => ({
|
||||||
|
createGitHubClient: mockCreateGitHubClient
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the concurrency module
|
||||||
|
const mockProcessWithResilience = mock(() => Promise.resolve([]));
|
||||||
|
|
||||||
|
mock.module("@/lib/utils/concurrency", () => ({
|
||||||
|
processWithResilience: mockProcessWithResilience
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock drizzle-orm
|
||||||
|
mock.module("drizzle-orm", () => ({
|
||||||
|
eq: mock(() => ({})),
|
||||||
|
inArray: mock(() => ({}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the types
|
||||||
|
mock.module("@/types/Repository", () => ({
|
||||||
|
repositoryVisibilityEnum: {
|
||||||
|
parse: mock((value: string) => value)
|
||||||
|
},
|
||||||
|
repoStatusEnum: {
|
||||||
|
parse: mock((value: string) => value)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
describe("Repository Mirroring API", () => {
|
describe("Repository Mirroring API", () => {
|
||||||
// Mock console.log and console.error to prevent test output noise
|
// Mock console.log and console.error to prevent test output noise
|
||||||
let originalConsoleLog: typeof console.log;
|
let originalConsoleLog: typeof console.log;
|
||||||
@@ -58,12 +132,13 @@ describe("Repository Mirroring API", () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await mockModule.POST({ request } as any);
|
const response = await POST({ request } as any);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
expect(data.error).toBe("Missing userId or repositoryIds.");
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.message).toBe("userId and repositoryIds are required.");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns 400 if repositoryIds is missing", async () => {
|
test("returns 400 if repositoryIds is missing", async () => {
|
||||||
@@ -77,12 +152,13 @@ describe("Repository Mirroring API", () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await mockModule.POST({ request } as any);
|
const response = await POST({ request } as any);
|
||||||
|
|
||||||
expect(response.status).toBe(400);
|
expect(response.status).toBe(400);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
expect(data.error).toBe("Missing userId or repositoryIds.");
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.message).toBe("userId and repositoryIds are required.");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns 200 and starts mirroring repositories", async () => {
|
test("returns 200 and starts mirroring repositories", async () => {
|
||||||
@@ -97,13 +173,13 @@ describe("Repository Mirroring API", () => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await mockModule.POST({ request } as any);
|
const response = await POST({ request } as any);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
expect(data.success).toBe(true);
|
expect(data.success).toBe(true);
|
||||||
expect(data.message).toBe("Repository mirroring started");
|
expect(data.message).toBe("Mirror job started.");
|
||||||
expect(data.batchId).toBe("test-batch-id");
|
expect(data.repositories).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
|||||||
import {
|
import {
|
||||||
mirrorGithubRepoToGitea,
|
mirrorGithubRepoToGitea,
|
||||||
mirrorGitHubOrgRepoToGiteaOrg,
|
mirrorGitHubOrgRepoToGiteaOrg,
|
||||||
|
getGiteaRepoOwnerAsync,
|
||||||
} from "@/lib/gitea";
|
} from "@/lib/gitea";
|
||||||
import { createGitHubClient } from "@/lib/github";
|
import { createGitHubClient } from "@/lib/github";
|
||||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -77,9 +78,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
// Define the concurrency limit - adjust based on API rate limits
|
// Define the concurrency limit - adjust based on API rate limits
|
||||||
const CONCURRENCY_LIMIT = 3;
|
const CONCURRENCY_LIMIT = 3;
|
||||||
|
|
||||||
// Generate a batch ID to group related repositories
|
|
||||||
const batchId = uuidv4();
|
|
||||||
|
|
||||||
// Process repositories in parallel with resilience to container restarts
|
// Process repositories in parallel with resilience to container restarts
|
||||||
await processWithResilience(
|
await processWithResilience(
|
||||||
repos,
|
repos,
|
||||||
@@ -99,12 +97,29 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
// Log the start of mirroring
|
// Log the start of mirroring
|
||||||
console.log(`Starting mirror for repository: ${repo.name}`);
|
console.log(`Starting mirror for repository: ${repo.name}`);
|
||||||
|
|
||||||
// Mirror the repository based on whether it's in an organization
|
// Determine where the repository should be mirrored (with organization overrides)
|
||||||
if (repo.organization && config.githubConfig.preserveOrgStructure) {
|
const owner = await getGiteaRepoOwnerAsync({
|
||||||
|
config,
|
||||||
|
repository: repoData,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Repository ${repo.name} will be mirrored to owner: ${owner}`);
|
||||||
|
|
||||||
|
// For single-org and starred repos strategies, or when mirroring to an org,
|
||||||
|
// always use the org mirroring function to ensure proper organization handling
|
||||||
|
const mirrorStrategy = config.giteaConfig?.mirrorStrategy ||
|
||||||
|
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
|
|
||||||
|
const shouldUseOrgMirror =
|
||||||
|
owner !== config.giteaConfig?.username || // Different owner means org
|
||||||
|
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
||||||
|
repoData.isStarred; // Starred repos always go to org
|
||||||
|
|
||||||
|
if (shouldUseOrgMirror) {
|
||||||
await mirrorGitHubOrgRepoToGiteaOrg({
|
await mirrorGitHubOrgRepoToGiteaOrg({
|
||||||
config,
|
config,
|
||||||
octokit,
|
octokit,
|
||||||
orgName: repo.organization,
|
orgName: owner,
|
||||||
repository: repoData,
|
repository: repoData,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -120,7 +135,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
{
|
{
|
||||||
userId: config.userId || "",
|
userId: config.userId || "",
|
||||||
jobType: "mirror",
|
jobType: "mirror",
|
||||||
batchId,
|
|
||||||
getItemId: (repo) => repo.id,
|
getItemId: (repo) => repo.id,
|
||||||
getItemName: (repo) => repo.name,
|
getItemName: (repo) => repo.name,
|
||||||
concurrencyLimit: CONCURRENCY_LIMIT,
|
concurrencyLimit: CONCURRENCY_LIMIT,
|
||||||
@@ -129,15 +143,19 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
checkpointInterval: 5, // Checkpoint every 5 repositories to reduce event frequency
|
checkpointInterval: 5, // Checkpoint every 5 repositories to reduce event frequency
|
||||||
onProgress: (completed, total, result) => {
|
onProgress: (completed, total, result) => {
|
||||||
const percentComplete = Math.round((completed / total) * 100);
|
const percentComplete = Math.round((completed / total) * 100);
|
||||||
console.log(`Mirroring progress: ${percentComplete}% (${completed}/${total})`);
|
console.log(
|
||||||
|
`Mirroring progress: ${percentComplete}% (${completed}/${total})`
|
||||||
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log(`Successfully mirrored repository: ${result.name}`);
|
console.log(`Successfully mirrored repository: ${result.name}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRetry: (repo, error, attempt) => {
|
onRetry: (repo, error, attempt) => {
|
||||||
console.log(`Retrying repository ${repo.name} (attempt ${attempt}): ${error.message}`);
|
console.log(
|
||||||
}
|
`Retrying repository ${repo.name} (attempt ${attempt}): ${error.message}`
|
||||||
|
);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -168,7 +186,10 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
// Enhanced error logging for better debugging
|
// Enhanced error logging for better debugging
|
||||||
console.error("=== ERROR MIRRORING REPOSITORIES ===");
|
console.error("=== ERROR MIRRORING REPOSITORIES ===");
|
||||||
console.error("Error type:", error?.constructor?.name);
|
console.error("Error type:", error?.constructor?.name);
|
||||||
console.error("Error message:", error instanceof Error ? error.message : String(error));
|
console.error(
|
||||||
|
"Error message:",
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.error("Error stack:", error.stack);
|
console.error("Error stack:", error.stack);
|
||||||
@@ -181,9 +202,11 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
console.error("- Headers:", Object.fromEntries(request.headers.entries()));
|
console.error("- Headers:", Object.fromEntries(request.headers.entries()));
|
||||||
|
|
||||||
// If it's a JSON parsing error, provide more context
|
// If it's a JSON parsing error, provide more context
|
||||||
if (error instanceof SyntaxError && error.message.includes('JSON')) {
|
if (error instanceof SyntaxError && error.message.includes("JSON")) {
|
||||||
console.error("🚨 JSON PARSING ERROR DETECTED:");
|
console.error("🚨 JSON PARSING ERROR DETECTED:");
|
||||||
console.error("This suggests the response from Gitea API is not valid JSON");
|
console.error(
|
||||||
|
"This suggests the response from Gitea API is not valid JSON"
|
||||||
|
);
|
||||||
console.error("Common causes:");
|
console.error("Common causes:");
|
||||||
console.error("- Gitea server returned HTML error page instead of JSON");
|
console.error("- Gitea server returned HTML error page instead of JSON");
|
||||||
console.error("- Network connection interrupted");
|
console.error("- Network connection interrupted");
|
||||||
@@ -194,16 +217,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
console.error("=====================================");
|
console.error("=====================================");
|
||||||
|
|
||||||
return new Response(
|
return createSecureErrorResponse(error, "mirror-repo API", 500);
|
||||||
JSON.stringify({
|
|
||||||
error: error instanceof Error ? error.message : "An unknown error occurred",
|
|
||||||
errorType: error?.constructor?.name || "Unknown",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
troubleshooting: error instanceof SyntaxError && error.message.includes('JSON')
|
|
||||||
? "JSON parsing error detected. Check Gitea server status and logs. Ensure Gitea is returning valid JSON responses."
|
|
||||||
: "Check application logs for more details"
|
|
||||||
}),
|
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { db, configs, repositories } from "@/lib/db";
|
import { db, configs, repositories } from "@/lib/db";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import { getGiteaRepoOwner, isRepoPresentInGitea } from "@/lib/gitea";
|
import { getGiteaRepoOwnerAsync, isRepoPresentInGitea } from "@/lib/gitea";
|
||||||
import {
|
import {
|
||||||
mirrorGithubRepoToGitea,
|
mirrorGithubRepoToGitea,
|
||||||
mirrorGitHubOrgRepoToGiteaOrg,
|
mirrorGitHubOrgRepoToGiteaOrg,
|
||||||
@@ -12,6 +12,7 @@ import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository";
|
|||||||
import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry";
|
import type { RetryRepoRequest, RetryRepoResponse } from "@/types/retry";
|
||||||
import { processWithRetry } from "@/lib/utils/concurrency";
|
import { processWithRetry } from "@/lib/utils/concurrency";
|
||||||
import { createMirrorJob } from "@/lib/helpers";
|
import { createMirrorJob } from "@/lib/helpers";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -92,6 +93,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
lastMirrored: repo.lastMirrored ?? undefined,
|
lastMirrored: repo.lastMirrored ?? undefined,
|
||||||
errorMessage: repo.errorMessage ?? undefined,
|
errorMessage: repo.errorMessage ?? undefined,
|
||||||
forkedFrom: repo.forkedFrom ?? undefined,
|
forkedFrom: repo.forkedFrom ?? undefined,
|
||||||
|
mirroredLocation: repo.mirroredLocation || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the start of retry operation
|
// Log the start of retry operation
|
||||||
@@ -107,8 +109,8 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
status: "imported",
|
status: "imported",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine if the repository exists in Gitea
|
// Determine if the repository exists in Gitea (with organization overrides)
|
||||||
let owner = getGiteaRepoOwner({
|
let owner = await getGiteaRepoOwnerAsync({
|
||||||
config,
|
config,
|
||||||
repository: repoData,
|
repository: repoData,
|
||||||
});
|
});
|
||||||
@@ -133,13 +135,23 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
throw new Error("Octokit client is not initialized.");
|
throw new Error("Octokit client is not initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Importing repo: ${repo.name} ${owner}`);
|
console.log(`Importing repo: ${repo.name} to owner: ${owner}`);
|
||||||
|
|
||||||
if (repo.organization && config.githubConfig.preserveOrgStructure) {
|
// For single-org and starred repos strategies, or when mirroring to an org,
|
||||||
|
// always use the org mirroring function to ensure proper organization handling
|
||||||
|
const mirrorStrategy = config.giteaConfig?.mirrorStrategy ||
|
||||||
|
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
|
|
||||||
|
const shouldUseOrgMirror =
|
||||||
|
owner !== config.giteaConfig?.username || // Different owner means org
|
||||||
|
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
||||||
|
repoData.isStarred; // Starred repos always go to org
|
||||||
|
|
||||||
|
if (shouldUseOrgMirror) {
|
||||||
await mirrorGitHubOrgRepoToGiteaOrg({
|
await mirrorGitHubOrgRepoToGiteaOrg({
|
||||||
config,
|
config,
|
||||||
octokit,
|
octokit,
|
||||||
orgName: repo.organization,
|
orgName: owner,
|
||||||
repository: {
|
repository: {
|
||||||
...repoData,
|
...repoData,
|
||||||
status: repoStatusEnum.parse("imported"),
|
status: repoStatusEnum.parse("imported"),
|
||||||
@@ -199,12 +211,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error retrying repo:", err);
|
return createSecureErrorResponse(err, "repository retry", 500);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: err instanceof Error ? err.message : "An unknown error occurred",
|
|
||||||
}),
|
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
ScheduleSyncRepoRequest,
|
ScheduleSyncRepoRequest,
|
||||||
ScheduleSyncRepoResponse,
|
ScheduleSyncRepoResponse,
|
||||||
} from "@/types/sync";
|
} from "@/types/sync";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -111,6 +112,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
organization: repo.organization ?? undefined,
|
organization: repo.organization ?? undefined,
|
||||||
lastMirrored: repo.lastMirrored ?? undefined,
|
lastMirrored: repo.lastMirrored ?? undefined,
|
||||||
errorMessage: repo.errorMessage ?? undefined,
|
errorMessage: repo.errorMessage ?? undefined,
|
||||||
|
mirroredLocation: repo.mirroredLocation || "",
|
||||||
forkedFrom: repo.forkedFrom ?? undefined,
|
forkedFrom: repo.forkedFrom ?? undefined,
|
||||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||||
},
|
},
|
||||||
@@ -132,6 +134,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
errorMessage: repo.errorMessage ?? undefined,
|
errorMessage: repo.errorMessage ?? undefined,
|
||||||
forkedFrom: repo.forkedFrom ?? undefined,
|
forkedFrom: repo.forkedFrom ?? undefined,
|
||||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||||
|
mirroredLocation: repo.mirroredLocation || "",
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,15 +143,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in scheduling sync:", error);
|
return createSecureErrorResponse(error, "schedule sync", 500);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
error instanceof Error ? error.message : "An unknown error occurred",
|
|
||||||
repositories: [],
|
|
||||||
} satisfies ScheduleSyncRepoResponse),
|
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
|||||||
import { syncGiteaRepo } from "@/lib/gitea";
|
import { syncGiteaRepo } from "@/lib/gitea";
|
||||||
import type { SyncRepoResponse } from "@/types/sync";
|
import type { SyncRepoResponse } from "@/types/sync";
|
||||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -67,9 +67,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
// Define the concurrency limit - adjust based on API rate limits
|
// Define the concurrency limit - adjust based on API rate limits
|
||||||
const CONCURRENCY_LIMIT = 5;
|
const CONCURRENCY_LIMIT = 5;
|
||||||
|
|
||||||
// Generate a batch ID to group related repositories
|
|
||||||
const batchId = uuidv4();
|
|
||||||
|
|
||||||
// Process repositories in parallel with resilience to container restarts
|
// Process repositories in parallel with resilience to container restarts
|
||||||
await processWithResilience(
|
await processWithResilience(
|
||||||
repos,
|
repos,
|
||||||
@@ -83,6 +80,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
errorMessage: repo.errorMessage ?? undefined,
|
errorMessage: repo.errorMessage ?? undefined,
|
||||||
forkedFrom: repo.forkedFrom ?? undefined,
|
forkedFrom: repo.forkedFrom ?? undefined,
|
||||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||||
|
mirroredLocation: repo.mirroredLocation || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the start of syncing
|
// Log the start of syncing
|
||||||
@@ -99,7 +97,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
{
|
{
|
||||||
userId: config.userId || "",
|
userId: config.userId || "",
|
||||||
jobType: "sync",
|
jobType: "sync",
|
||||||
batchId,
|
|
||||||
getItemId: (repo) => repo.id,
|
getItemId: (repo) => repo.id,
|
||||||
getItemName: (repo) => repo.name,
|
getItemName: (repo) => repo.name,
|
||||||
concurrencyLimit: CONCURRENCY_LIMIT,
|
concurrencyLimit: CONCURRENCY_LIMIT,
|
||||||
@@ -134,6 +131,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
errorMessage: repo.errorMessage ?? undefined,
|
errorMessage: repo.errorMessage ?? undefined,
|
||||||
forkedFrom: repo.forkedFrom ?? undefined,
|
forkedFrom: repo.forkedFrom ?? undefined,
|
||||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||||
|
mirroredLocation: repo.mirroredLocation || "",
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,13 +141,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in syncing repositories:", error);
|
return createSecureErrorResponse(error, "repository sync", 500);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error:
|
|
||||||
error instanceof Error ? error.message : "An unknown error occurred",
|
|
||||||
}),
|
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
82
src/pages/api/organizations/[id].ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
import { db, organizations } from "@/lib/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||||
|
|
||||||
|
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||||
|
try {
|
||||||
|
// Get token from Authorization header or cookies
|
||||||
|
const authHeader = request.headers.get("Authorization");
|
||||||
|
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token and get user ID
|
||||||
|
let userId: string;
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||||
|
userId = decoded.id;
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgId = params.id;
|
||||||
|
if (!orgId) {
|
||||||
|
return new Response(JSON.stringify({ error: "Organization ID is required" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { destinationOrg } = body;
|
||||||
|
|
||||||
|
// Validate that the organization belongs to the user
|
||||||
|
const [existingOrg] = await db
|
||||||
|
.select()
|
||||||
|
.from(organizations)
|
||||||
|
.where(and(eq(organizations.id, orgId), eq(organizations.userId, userId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingOrg) {
|
||||||
|
return new Response(JSON.stringify({ error: "Organization not found" }), {
|
||||||
|
status: 404,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the organization's destination override
|
||||||
|
await db
|
||||||
|
.update(organizations)
|
||||||
|
.set({
|
||||||
|
destinationOrg: destinationOrg || null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(organizations.id, orgId));
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Organization destination updated successfully",
|
||||||
|
destinationOrg: destinationOrg || null,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return createSecureErrorResponse(error, "Update organization destination", 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
82
src/pages/api/repositories/[id].ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
import { db, repositories } from "@/lib/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||||
|
|
||||||
|
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||||
|
try {
|
||||||
|
// Get token from Authorization header or cookies
|
||||||
|
const authHeader = request.headers.get("Authorization");
|
||||||
|
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token and get user ID
|
||||||
|
let userId: string;
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||||
|
userId = decoded.id;
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoId = params.id;
|
||||||
|
if (!repoId) {
|
||||||
|
return new Response(JSON.stringify({ error: "Repository ID is required" }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { destinationOrg } = body;
|
||||||
|
|
||||||
|
// Validate that the repository belongs to the user
|
||||||
|
const [existingRepo] = await db
|
||||||
|
.select()
|
||||||
|
.from(repositories)
|
||||||
|
.where(and(eq(repositories.id, repoId), eq(repositories.userId, userId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingRepo) {
|
||||||
|
return new Response(JSON.stringify({ error: "Repository not found" }), {
|
||||||
|
status: 404,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the repository's destination override
|
||||||
|
await db
|
||||||
|
.update(repositories)
|
||||||
|
.set({
|
||||||
|
destinationOrg: destinationOrg || null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(repositories.id, repoId));
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
message: "Repository destination updated successfully",
|
||||||
|
destinationOrg: destinationOrg || null,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return createSecureErrorResponse(error, "Update repository destination", 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
getGithubRepositories,
|
getGithubRepositories,
|
||||||
getGithubStarredRepositories,
|
getGithubStarredRepositories,
|
||||||
} from "@/lib/github";
|
} from "@/lib/github";
|
||||||
import { jsonResponse } from "@/lib/utils";
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
@@ -166,12 +166,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error syncing GitHub data for user:", userId, error);
|
return createSecureErrorResponse(error, "GitHub data sync", 500);
|
||||||
return jsonResponse({
|
|
||||||
data: {
|
|
||||||
error: error instanceof Error ? error.message : "Something went wrong",
|
|
||||||
},
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { APIRoute } from "astro";
|
|||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { configs, db, organizations, repositories } from "@/lib/db";
|
import { configs, db, organizations, repositories } from "@/lib/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { jsonResponse } from "@/lib/utils";
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||||
import type {
|
import type {
|
||||||
AddOrganizationApiRequest,
|
AddOrganizationApiRequest,
|
||||||
AddOrganizationApiResponse,
|
AddOrganizationApiResponse,
|
||||||
@@ -125,12 +125,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
return jsonResponse({ data: resPayload, status: 200 });
|
return jsonResponse({ data: resPayload, status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error inserting organization/repositories:", error);
|
return createSecureErrorResponse(error, "organization sync", 500);
|
||||||
return jsonResponse({
|
|
||||||
data: {
|
|
||||||
error: error instanceof Error ? error.message : "Something went wrong",
|
|
||||||
},
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { configs, db, repositories } from "@/lib/db";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { type Repository } from "@/lib/db/schema";
|
import { type Repository } from "@/lib/db/schema";
|
||||||
import { jsonResponse } from "@/lib/utils";
|
import { jsonResponse, createSecureErrorResponse } from "@/lib/utils";
|
||||||
import type {
|
import type {
|
||||||
AddRepositoriesApiRequest,
|
AddRepositoriesApiRequest,
|
||||||
AddRepositoriesApiResponse,
|
AddRepositoriesApiResponse,
|
||||||
@@ -97,6 +97,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
status: "imported" as Repository["status"],
|
status: "imported" as Repository["status"],
|
||||||
lastMirrored: undefined,
|
lastMirrored: undefined,
|
||||||
errorMessage: undefined,
|
errorMessage: undefined,
|
||||||
|
mirroredLocation: "",
|
||||||
createdAt: repoData.created_at
|
createdAt: repoData.created_at
|
||||||
? new Date(repoData.created_at)
|
? new Date(repoData.created_at)
|
||||||
: new Date(),
|
: new Date(),
|
||||||
@@ -126,12 +127,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
return jsonResponse({ data: resPayload, status: 200 });
|
return jsonResponse({ data: resPayload, status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error inserting repository:", error);
|
return createSecureErrorResponse(error, "repository sync", 500);
|
||||||
return jsonResponse({
|
|
||||||
data: {
|
|
||||||
error: error instanceof Error ? error.message : "Something went wrong",
|
|
||||||
},
|
|
||||||
status: 500,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
import { publishEvent } from "@/lib/events";
|
import { publishEvent } from "@/lib/events";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -44,13 +45,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
{ status: 200 }
|
{ status: 200 }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error publishing test event:", error);
|
return createSecureErrorResponse(error, "test-event API", 500);
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
error: "Failed to publish event",
|
|
||||||
details: error instanceof Error ? error.message : String(error),
|
|
||||||
}),
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import MainLayout from '../../layouts/main.astro';
|
|
||||||
|
|
||||||
// Enable prerendering for this dynamic route
|
|
||||||
export const prerender = true;
|
|
||||||
|
|
||||||
// Generate static paths for all documentation pages
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const docs = await getCollection('docs');
|
|
||||||
return docs.map(entry => ({
|
|
||||||
params: { slug: entry.slug },
|
|
||||||
props: { entry },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the documentation entry from props
|
|
||||||
const { entry } = Astro.props;
|
|
||||||
const { Content } = await entry.render();
|
|
||||||
---
|
|
||||||
|
|
||||||
<MainLayout title={entry.data.title}>
|
|
||||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
|
||||||
<div class="sticky top-4 z-10 mb-6">
|
|
||||||
<a
|
|
||||||
href="/docs/"
|
|
||||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">←</span> Back to Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<article class="bg-card rounded-2xl shadow-lg p-6 border border-border">
|
|
||||||
<div class="prose prose-neutral dark:prose-invert prose-code:bg-muted prose-code:text-foreground prose-pre:bg-muted prose-pre:text-foreground prose-pre:rounded-lg prose-pre:p-4 prose-table:rounded-lg prose-table:bg-muted prose-th:text-foreground prose-td:text-muted-foreground prose-blockquote:border-l-4 prose-blockquote:border-muted prose-blockquote:bg-muted/50 prose-blockquote:p-4">
|
|
||||||
<Content />
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<script type="module">
|
|
||||||
// Mermaid diagram rendering for code blocks
|
|
||||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
||||||
mermaid.initialize({ startOnLoad: false, theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default' });
|
|
||||||
function renderMermaidDiagrams() {
|
|
||||||
document.querySelectorAll('pre code.language-mermaid').forEach((block, i) => {
|
|
||||||
const parent = block.parentElement;
|
|
||||||
if (!parent) return;
|
|
||||||
const code = block.textContent;
|
|
||||||
const id = `mermaid-diagram-${i}`;
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.className = 'my-6';
|
|
||||||
container.id = id;
|
|
||||||
parent.replaceWith(container);
|
|
||||||
mermaid.render(id, code, (svgCode) => {
|
|
||||||
container.innerHTML = svgCode;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', renderMermaidDiagrams);
|
|
||||||
} else {
|
|
||||||
renderMermaidDiagrams();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</main>
|
|
||||||
</MainLayout>
|
|
||||||
335
src/pages/docs/architecture.astro
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
---
|
||||||
|
import MainLayout from '../../layouts/main.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout title="Architecture - Gitea Mirror">
|
||||||
|
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||||
|
<div class="sticky top-4 z-10 mb-6">
|
||||||
|
<a
|
||||||
|
href="/docs/"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">←</span> Back to Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="bg-card rounded-2xl shadow-lg p-6 md:p-8 border border-border">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-12 space-y-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm text-muted-foreground mb-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-1 4h1m-1 4h1"/>
|
||||||
|
</svg>
|
||||||
|
<span>Architecture Overview</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl font-bold tracking-tight">Gitea Mirror Architecture</h1>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed max-w-4xl">
|
||||||
|
This document provides a comprehensive overview of the Gitea Mirror application architecture, including component diagrams, project structure, and detailed explanations of each part of the system.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Overview -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">System Overview</h2>
|
||||||
|
|
||||||
|
<div class="bg-card/50 border border-border/50 rounded-lg p-6 mb-8">
|
||||||
|
<p class="text-base leading-relaxed mb-6">
|
||||||
|
Gitea Mirror is a web application that automates the mirroring of GitHub repositories to Gitea instances. It provides a user-friendly interface for configuring, monitoring, and managing mirroring operations without requiring users to edit configuration files or run Docker commands.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">Technology Stack</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{[
|
||||||
|
{ name: 'Astro', desc: 'Web framework for Server-Side Rendering (SSR)' },
|
||||||
|
{ name: 'React', desc: 'Component library for interactive UI elements' },
|
||||||
|
{ name: 'Tailwind CSS v4', desc: 'Utility-first CSS framework (with Vite plugin)' },
|
||||||
|
{ name: 'Shadcn UI', desc: 'UI component library built on Tailwind CSS' },
|
||||||
|
{ name: 'SQLite', desc: 'Database for storing configuration, state, and events' },
|
||||||
|
{ name: 'Bun', desc: 'JavaScript runtime and package manager' },
|
||||||
|
{ name: 'Drizzle ORM', desc: 'Type-safe ORM for database interactions' }
|
||||||
|
].map(tech => (
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="w-2 h-2 rounded-full bg-primary mt-2"></div>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold text-foreground">{tech.name}</span>
|
||||||
|
<p class="text-sm text-muted-foreground">{tech.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Architecture Diagram -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Architecture Diagram</h2>
|
||||||
|
|
||||||
|
<div class="my-8">
|
||||||
|
<div class="architecture-diagram bg-muted/30 rounded-xl p-8 border border-border">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
<!-- Gitea Mirror System -->
|
||||||
|
<div class="bg-card rounded-lg border-2 border-primary/20 p-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-6 text-center text-primary">Gitea Mirror System</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
{[
|
||||||
|
{ icon: '🎨', name: 'Frontend', tech: 'Astro + React' },
|
||||||
|
{ icon: '⚙️', name: 'Backend', tech: 'Bun Runtime' },
|
||||||
|
{ icon: '🗄️', name: 'Database', tech: 'SQLite + Drizzle' }
|
||||||
|
].map((component, index) => (
|
||||||
|
<>
|
||||||
|
<div class="bg-primary/10 rounded-lg p-4 text-center">
|
||||||
|
<div class="text-2xl mb-2">{component.icon}</div>
|
||||||
|
<h4 class="font-semibold">{component.name}</h4>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">{component.tech}</p>
|
||||||
|
</div>
|
||||||
|
{index < 2 && (
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- External APIs -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-card rounded-lg border-2 border-amber-500/20 p-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-6 text-center text-amber-600 dark:text-amber-500">External APIs</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-amber-500/10 rounded-lg p-4 text-center">
|
||||||
|
<div class="text-2xl mb-2">🐙</div>
|
||||||
|
<h4 class="font-semibold">GitHub API</h4>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">Repository Data Source</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-amber-500/10 rounded-lg p-4 text-center">
|
||||||
|
<div class="text-2xl mb-2">🍵</div>
|
||||||
|
<h4 class="font-semibold">Gitea API</h4>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">Mirror Destination</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center gap-4 mt-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-sm text-muted-foreground mb-2">Data Flow</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-primary font-semibold">Backend</span>
|
||||||
|
<svg class="w-6 h-6 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-amber-600 dark:text-amber-500 font-semibold">APIs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Component Breakdown -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Component Breakdown</h2>
|
||||||
|
|
||||||
|
<!-- Frontend -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Frontend (Astro + React)</h3>
|
||||||
|
<div class="pl-4 border-l-2 border-primary/20">
|
||||||
|
<p class="text-muted-foreground mb-4">
|
||||||
|
The frontend is built with Astro, a modern web framework that allows for server-side rendering and partial hydration. React components are used for interactive elements, providing a responsive and dynamic user interface.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4 class="font-semibold mb-3">Key Frontend Components</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
{[
|
||||||
|
{ name: 'Dashboard', desc: 'Overview of mirroring status and recent activity' },
|
||||||
|
{ name: 'Repository Management', desc: 'Interface for managing repositories to mirror' },
|
||||||
|
{ name: 'Organization Management', desc: 'Interface for managing GitHub organizations' },
|
||||||
|
{ name: 'Configuration', desc: 'Settings for GitHub and Gitea connections' },
|
||||||
|
{ name: 'Activity Log', desc: 'Detailed log of mirroring operations' }
|
||||||
|
].map(component => (
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<span class="text-primary font-mono text-sm">▸</span>
|
||||||
|
<div>
|
||||||
|
<strong>{component.name}</strong>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">{component.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backend -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Backend (Bun)</h3>
|
||||||
|
<div class="pl-4 border-l-2 border-primary/20">
|
||||||
|
<p class="text-muted-foreground mb-4">
|
||||||
|
The backend is built with Bun and provides API endpoints for the frontend to interact with. It handles:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
{[
|
||||||
|
'Authentication and user management',
|
||||||
|
'GitHub API integration',
|
||||||
|
'Gitea API integration',
|
||||||
|
'Mirroring operations and job queue',
|
||||||
|
'Real-time updates via Server-Sent Events (SSE) at /api/sse/',
|
||||||
|
'Job recovery system for interrupted operations',
|
||||||
|
'Graceful shutdown handling',
|
||||||
|
'Scheduled automatic mirroring',
|
||||||
|
'Database interactions with Drizzle ORM'
|
||||||
|
].map(item => (
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<span class="text-primary font-mono text-sm">▸</span>
|
||||||
|
<span>{item}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Database -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Database (SQLite + Drizzle ORM)</h3>
|
||||||
|
<div class="pl-4 border-l-2 border-primary/20">
|
||||||
|
<p class="text-muted-foreground mb-4">
|
||||||
|
SQLite with Bun's native SQLite driver is used for data persistence, with Drizzle ORM providing type-safe database interactions. The database stores:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
{[
|
||||||
|
'User accounts and authentication data',
|
||||||
|
'GitHub and Gitea configuration',
|
||||||
|
'Repository and organization information',
|
||||||
|
'Mirroring job history and status',
|
||||||
|
'Event notifications and their read status'
|
||||||
|
].map(item => (
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<span class="text-primary font-mono text-sm">▸</span>
|
||||||
|
<span>{item}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Data Flow -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Data Flow</h2>
|
||||||
|
|
||||||
|
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary">
|
||||||
|
<ol class="space-y-4">
|
||||||
|
{[
|
||||||
|
{ title: 'User Authentication', desc: 'Users authenticate through the frontend, which communicates with the backend to validate credentials.' },
|
||||||
|
{ title: 'Configuration', desc: 'Users configure GitHub and Gitea settings through the UI, which are stored in the SQLite database.' },
|
||||||
|
{ title: 'Repository Discovery', desc: 'The backend queries the GitHub API to discover repositories based on user configuration.' },
|
||||||
|
{ title: 'Mirroring Process', desc: 'When triggered, the backend fetches repository data from GitHub and pushes it to Gitea.' },
|
||||||
|
{ title: 'Status Tracking', desc: 'All operations are logged in the database and displayed in the Activity Log.' }
|
||||||
|
].map((step, index) => (
|
||||||
|
<li class="flex gap-4">
|
||||||
|
<span class="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<strong class="text-foreground">{step.title}</strong>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">{step.desc}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Project Structure -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Project Structure</h2>
|
||||||
|
|
||||||
|
<div class="bg-muted/30 rounded-lg p-4">
|
||||||
|
<pre class="text-sm"><code>{`gitea-mirror/
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── components/ # React components
|
||||||
|
│ ├── content/ # Documentation and content
|
||||||
|
│ ├── layouts/ # Astro layout components
|
||||||
|
│ ├── lib/ # Utility functions and database
|
||||||
|
│ ├── pages/ # Astro pages and API routes
|
||||||
|
│ └── styles/ # CSS and Tailwind styles
|
||||||
|
├── public/ # Static assets
|
||||||
|
├── data/ # Database and persistent data
|
||||||
|
├── docker/ # Docker configuration
|
||||||
|
└── scripts/ # Utility scripts for deployment and maintenance
|
||||||
|
├── gitea-mirror-lxc-local.sh # Local LXC deployment script
|
||||||
|
└── manage-db.ts # Database management tool`}</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Deployment Options -->
|
||||||
|
<section>
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Deployment Options</h2>
|
||||||
|
|
||||||
|
<p class="text-muted-foreground mb-6">Gitea Mirror supports multiple deployment options:</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||||
|
{[
|
||||||
|
{ icon: '🐳', name: 'Docker', desc: 'Run as a containerized application using Docker and docker-compose' },
|
||||||
|
{ icon: '📦', name: 'LXC Containers', desc: 'Deploy in Linux Containers on Proxmox VE using community script' },
|
||||||
|
{ icon: '🏃', name: 'Native', desc: 'Run directly on the host system using Bun runtime' }
|
||||||
|
].map(option => (
|
||||||
|
<div class="bg-card rounded-lg border border-border p-4 hover:border-primary/50 transition-colors">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="text-2xl">{option.icon}</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-1">{option.name}</h4>
|
||||||
|
<p class="text-sm text-muted-foreground">{option.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-muted/30 rounded-lg p-4 mt-6">
|
||||||
|
<h4 class="font-semibold mb-3">Deployment Advantages</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<strong class="text-primary">Docker:</strong>
|
||||||
|
<span class="text-muted-foreground">Isolation, easy updates, consistent environment</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<strong class="text-primary">LXC:</strong>
|
||||||
|
<span class="text-muted-foreground">Lightweight virtualization, better performance than Docker, system-level isolation</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<strong class="text-primary">Native:</strong>
|
||||||
|
<span class="text-muted-foreground">Best performance, direct access to system resources</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-sm text-muted-foreground">
|
||||||
|
<p><strong>Note:</strong> LXC deployment is available through the <a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror" class="text-primary hover:underline">Proxmox VE Community Scripts</a> project.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</MainLayout>
|
||||||
512
src/pages/docs/configuration.astro
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
---
|
||||||
|
import MainLayout from '../../layouts/main.astro';
|
||||||
|
|
||||||
|
const envVars = [
|
||||||
|
{ name: 'NODE_ENV', desc: 'Runtime environment', default: 'development', example: 'production' },
|
||||||
|
{ name: 'DATABASE_URL', desc: 'SQLite database URL', default: 'file:data/gitea-mirror.db', example: 'file:path/to/database.db' },
|
||||||
|
{ name: 'JWT_SECRET', desc: 'Secret key for JWT auth', default: 'Auto-generated', example: 'your-secure-string' },
|
||||||
|
{ name: 'HOST', desc: 'Server host', default: 'localhost', example: '0.0.0.0' },
|
||||||
|
{ name: 'PORT', desc: 'Server port', default: '4321', example: '8080' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const githubOptions = [
|
||||||
|
{ name: 'Username', desc: 'Your GitHub username', default: '-' },
|
||||||
|
{ name: 'Token', desc: 'GitHub personal access token (Classic PAT)', default: '-' },
|
||||||
|
{ name: 'Private Repositories', desc: 'Include private repositories', default: 'false' },
|
||||||
|
{ name: 'Mirror Starred', desc: 'Mirror repositories you\'ve starred', default: 'false' },
|
||||||
|
{ name: 'Mirror Issues', desc: 'Mirror issues from GitHub to Gitea', default: 'false' },
|
||||||
|
{ name: 'Mirror Wiki', desc: 'Mirror wiki pages from GitHub to Gitea', default: 'false' },
|
||||||
|
{ name: 'Mirror Organizations', desc: 'Mirror organization repositories', default: 'false' },
|
||||||
|
{ name: 'Only Mirror Orgs', desc: 'Only mirror organization repositories', default: 'false' },
|
||||||
|
{ name: 'Skip Forks', desc: 'Exclude repositories that are forks', default: 'false' },
|
||||||
|
{ name: 'Skip Starred Issues', desc: 'Skip issues for starred repositories', default: 'false' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const giteaOptions = [
|
||||||
|
{ name: 'URL', desc: 'Gitea server URL', default: '-' },
|
||||||
|
{ name: 'Token', desc: 'Gitea access token', default: '-' },
|
||||||
|
{ name: 'Organization', desc: 'Default organization for mirrored repositories', default: '-' },
|
||||||
|
{ name: 'Visibility', desc: 'Default visibility for mirrored repositories', default: 'public' },
|
||||||
|
{ name: 'Starred Repos Org', desc: 'Organization for starred repositories', default: 'github' }
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout title="Configuration - Gitea Mirror">
|
||||||
|
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||||
|
<div class="sticky top-4 z-10 mb-6">
|
||||||
|
<a
|
||||||
|
href="/docs/"
|
||||||
|
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">←</span> Back to Documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<article class="bg-card rounded-2xl shadow-lg p-6 md:p-8 border border-border">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-12 space-y-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm text-muted-foreground mb-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Configuration Guide</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl font-bold tracking-tight">Gitea Mirror Configuration</h1>
|
||||||
|
<p class="text-lg text-muted-foreground leading-relaxed max-w-4xl">
|
||||||
|
This guide provides detailed information on how to configure Gitea Mirror for your environment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Methods -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Configuration Methods</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||||
|
<div class="bg-card rounded-lg border border-border p-6 hover:border-primary/50 transition-colors">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="text-2xl">🔧</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-lg mb-2">Environment Variables</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">Set configuration options through environment variables for automated deployments</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-card rounded-lg border border-border p-6 hover:border-primary/50 transition-colors">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="text-2xl">🖥️</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-lg mb-2">Web UI</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">Configure the application through the web interface after installation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Environment Variables -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Environment Variables</h2>
|
||||||
|
|
||||||
|
<p class="text-muted-foreground mb-6">The following environment variables can be used to configure Gitea Mirror:</p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-border">
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Variable</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Example</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{envVars.map((v, i) => (
|
||||||
|
<tr class={`border-b border-border/50 hover:bg-muted/30 ${i === envVars.length - 1 ? 'border-b-0' : ''}`}>
|
||||||
|
<td class="py-3 px-4">
|
||||||
|
<code class="text-sm bg-muted px-1.5 py-0.5 rounded">{v.name}</code>
|
||||||
|
</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-muted-foreground">{v.desc}</td>
|
||||||
|
<td class="py-3 px-4 text-sm"><code>{v.default}</code></td>
|
||||||
|
<td class="py-3 px-4 text-sm"><code>{v.example}</code></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Security Note -->
|
||||||
|
<div class="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4 mt-6">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="text-amber-600 dark:text-amber-500">
|
||||||
|
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-amber-600 dark:text-amber-500 mb-1">Security Note</h4>
|
||||||
|
<p class="text-sm">The application will automatically generate a secure random <code class="bg-amber-500/10 px-1 py-0.5 rounded">JWT_SECRET</code> on first run if one isn't provided. This generated secret is stored in the data directory for persistence across container restarts.</p>
|
||||||
|
<p class="text-sm mt-2">While this auto-generation feature provides good security by default, you can still explicitly set your own <code class="bg-amber-500/10 px-1 py-0.5 rounded">JWT_SECRET</code> for complete control over your deployment.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Web UI Configuration -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Web UI Configuration</h2>
|
||||||
|
|
||||||
|
<p class="text-muted-foreground mb-6">After installing and starting Gitea Mirror, you can configure it through the web interface:</p>
|
||||||
|
|
||||||
|
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary mb-8">
|
||||||
|
<ol class="space-y-3">
|
||||||
|
{[
|
||||||
|
'Navigate to <code class="bg-muted px-1.5 py-0.5 rounded text-sm">http://your-server:port/</code>',
|
||||||
|
'If this is your first time, you\'ll be guided through creating an admin account',
|
||||||
|
'Log in with your credentials',
|
||||||
|
'Go to the Configuration page'
|
||||||
|
].map((step, i) => (
|
||||||
|
<li class="flex gap-3">
|
||||||
|
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">{i + 1}</span>
|
||||||
|
<span set:html={step}></span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GitHub Configuration -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">GitHub Configuration</h3>
|
||||||
|
<p class="text-muted-foreground mb-4">The GitHub configuration section allows you to connect to GitHub and specify which repositories to mirror.</p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto mb-6">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-border">
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Option</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{githubOptions.map((opt, i) => (
|
||||||
|
<tr class={`border-b border-border/50 hover:bg-muted/30 ${i === githubOptions.length - 1 ? 'border-b-0' : ''}`}>
|
||||||
|
<td class="py-3 px-4 font-medium">{opt.name}</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-muted-foreground">{opt.desc}</td>
|
||||||
|
<td class="py-3 px-4 text-sm"><code>{opt.default}</code></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- GitHub Token Permissions -->
|
||||||
|
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4 mb-6">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="text-blue-600 dark:text-blue-500">
|
||||||
|
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold text-blue-600 dark:text-blue-500 mb-2">Required Permissions</h4>
|
||||||
|
<p class="text-sm mb-3">You need to create a <span class="font-semibold">Classic GitHub PAT Token</span> with the following scopes:</p>
|
||||||
|
<ul class="space-y-1 text-sm">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-500">•</span>
|
||||||
|
<span><code class="bg-blue-500/10 px-1 py-0.5 rounded">repo</code> - Full control of private repositories</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-500">•</span>
|
||||||
|
<span><code class="bg-blue-500/10 px-1 py-0.5 rounded">admin:org</code> - Full control of orgs and teams, read and write org projects</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm mt-2">The organization access is required for mirroring organization repositories.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pl-4 border-l-2 border-primary/20">
|
||||||
|
<h5 class="font-semibold mb-3">To create a GitHub token:</h5>
|
||||||
|
<ol class="space-y-2 text-sm">
|
||||||
|
<li>Go to <a href="https://github.com/settings/tokens" class="text-primary hover:underline">GitHub Settings > Developer settings > Personal access tokens</a></li>
|
||||||
|
<li>Click "Generate new token"</li>
|
||||||
|
<li>Select the required permissions</li>
|
||||||
|
<li>Copy the generated token and paste it into Gitea Mirror</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gitea Configuration -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Gitea Configuration</h3>
|
||||||
|
<p class="text-muted-foreground mb-4">The Gitea configuration section allows you to connect to your Gitea instance and specify how repositories should be mirrored.</p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto mb-6">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-border">
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Option</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{giteaOptions.map((opt, i) => (
|
||||||
|
<tr class={`border-b border-border/50 hover:bg-muted/30 ${i === giteaOptions.length - 1 ? 'border-b-0' : ''}`}>
|
||||||
|
<td class="py-3 px-4 font-medium">{opt.name}</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-muted-foreground">{opt.desc}</td>
|
||||||
|
<td class="py-3 px-4 text-sm"><code>{opt.default}</code></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mirror Strategies -->
|
||||||
|
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary mb-6">
|
||||||
|
<h4 class="font-semibold text-lg mb-4">Mirror Strategies</h4>
|
||||||
|
<p class="text-sm text-muted-foreground mb-4">Choose how your repositories will be organized in Gitea:</p>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="bg-card rounded-lg border border-border p-4">
|
||||||
|
<h5 class="font-semibold text-base mb-2 flex items-center gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-500">📁</span>
|
||||||
|
Preserve GitHub Structure
|
||||||
|
</h5>
|
||||||
|
<p class="text-sm text-muted-foreground mb-2">Maintains the exact structure from GitHub:</p>
|
||||||
|
<ul class="space-y-1 text-sm ml-4">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-muted-foreground">•</span>
|
||||||
|
<span>Personal repos → Your Gitea username</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-muted-foreground">•</span>
|
||||||
|
<span>Organization repos → Same organization name in Gitea</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-card rounded-lg border border-border p-4">
|
||||||
|
<h5 class="font-semibold text-base mb-2 flex items-center gap-2">
|
||||||
|
<span class="text-purple-600 dark:text-purple-500">🏢</span>
|
||||||
|
Single Organization
|
||||||
|
</h5>
|
||||||
|
<p class="text-sm text-muted-foreground mb-2">Consolidates all repositories into one organization:</p>
|
||||||
|
<ul class="space-y-1 text-sm ml-4">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-muted-foreground">•</span>
|
||||||
|
<span>All repos → One designated organization</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-muted-foreground">•</span>
|
||||||
|
<span>Requires setting "Organization" field</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-card rounded-lg border border-border p-4">
|
||||||
|
<h5 class="font-semibold text-base mb-2 flex items-center gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-500">👤</span>
|
||||||
|
Flat User Structure
|
||||||
|
</h5>
|
||||||
|
<p class="text-sm text-muted-foreground mb-2">Mirrors all repositories under your user account:</p>
|
||||||
|
<ul class="space-y-1 text-sm ml-4">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-muted-foreground">•</span>
|
||||||
|
<span>All repos → Your Gitea username</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-muted-foreground">•</span>
|
||||||
|
<span>No organizations needed</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3 mt-4">
|
||||||
|
<p class="text-sm">
|
||||||
|
<span class="font-semibold">Note:</span> Starred repositories are always mirrored to the "Starred Repos Org" (default: "starred") regardless of the chosen strategy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pl-4 border-l-2 border-primary/20">
|
||||||
|
<h5 class="font-semibold mb-3">To create a Gitea access token:</h5>
|
||||||
|
<ol class="space-y-2 text-sm">
|
||||||
|
<li>Log in to your Gitea instance</li>
|
||||||
|
<li>Go to Settings > Applications</li>
|
||||||
|
<li>Under "Generate New Token", enter a name for your token</li>
|
||||||
|
<li>Click "Generate Token"</li>
|
||||||
|
<li>Copy the generated token and paste it into Gitea Mirror</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schedule Configuration -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Schedule Configuration</h3>
|
||||||
|
<p class="text-muted-foreground mb-4">You can configure automatic mirroring on a schedule:</p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-border">
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Option</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||||
|
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-border/50 hover:bg-muted/30">
|
||||||
|
<td class="py-3 px-4 font-medium">Enable Scheduling</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-muted-foreground">Enable automatic mirroring</td>
|
||||||
|
<td class="py-3 px-4 text-sm"><code>false</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr class="hover:bg-muted/30">
|
||||||
|
<td class="py-3 px-4 font-medium">Interval (seconds)</td>
|
||||||
|
<td class="py-3 px-4 text-sm text-muted-foreground">Time between mirroring operations</td>
|
||||||
|
<td class="py-3 px-4 text-sm"><code>3600</code> (1 hour)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="my-12 h-px bg-border/50"></div>
|
||||||
|
|
||||||
|
<!-- Advanced Configuration -->
|
||||||
|
<section class="mb-12">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Advanced Configuration</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Database Management -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Database Management</h3>
|
||||||
|
<p class="text-muted-foreground mb-4">Gitea Mirror includes several database management tools that can be run from the command line:</p>
|
||||||
|
|
||||||
|
<div class="bg-muted/30 rounded-lg p-4 overflow-x-auto">
|
||||||
|
<pre class="text-sm whitespace-pre-wrap break-all"><code>{`# Initialize the database (only if it doesn't exist)
|
||||||
|
bun run init-db
|
||||||
|
|
||||||
|
# Check database status
|
||||||
|
bun run check-db
|
||||||
|
|
||||||
|
# Fix database location issues
|
||||||
|
bun run fix-db
|
||||||
|
|
||||||
|
# Reset all users (for testing signup flow)
|
||||||
|
bun run reset-users
|
||||||
|
|
||||||
|
# Remove database files completely
|
||||||
|
bun run cleanup-db`}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event Management -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Event Management</h3>
|
||||||
|
<p class="text-muted-foreground mb-4">Events in Gitea Mirror (such as repository mirroring operations) are stored in the SQLite database and can be viewed in the Activity Log page.</p>
|
||||||
|
|
||||||
|
<div class="bg-green-500/10 border border-green-500/20 rounded-lg p-4">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="text-green-600 dark:text-green-500">
|
||||||
|
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm mb-2">Event Management Features:</p>
|
||||||
|
<ul class="space-y-1 text-sm">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-500">•</span>
|
||||||
|
<span>View all events with filtering by type, status, and search</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-500">•</span>
|
||||||
|
<span>Real-time updates via Server-Sent Events (SSE)</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-500">•</span>
|
||||||
|
<span>Clean up old events using the cleanup button in the Activity Log</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-500">•</span>
|
||||||
|
<span>Automatic cleanup with configurable retention period</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Automatic Recovery System -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Automatic Recovery System</h3>
|
||||||
|
<p class="text-muted-foreground mb-4">Gitea Mirror includes a robust recovery system that automatically handles interrupted operations:</p>
|
||||||
|
|
||||||
|
<div class="bg-card rounded-lg border border-border p-6">
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-primary">✓</span>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Startup Recovery:</span>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">Automatically recovers interrupted jobs when the application starts</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-primary">✓</span>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Graceful Shutdown:</span>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">Saves job state before shutting down to enable recovery on restart</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-primary">✓</span>
|
||||||
|
<div>
|
||||||
|
<span class="font-semibold">Job State Persistence:</span>
|
||||||
|
<p class="text-sm text-muted-foreground mt-1">Stores mirror job progress in the database for resilience</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-4 bg-muted/30 rounded-lg p-4 overflow-x-auto">
|
||||||
|
<p class="text-sm font-medium mb-2">Manual recovery tools:</p>
|
||||||
|
<pre class="text-sm whitespace-pre-wrap break-all"><code>{`# Run startup recovery manually
|
||||||
|
bun run startup-recovery
|
||||||
|
|
||||||
|
# Fix interrupted jobs
|
||||||
|
bun scripts/fix-interrupted-jobs.ts
|
||||||
|
|
||||||
|
# Test recovery system
|
||||||
|
bun run test-recovery`}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Health Check Endpoint -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-semibold mb-4">Health Check Endpoint</h3>
|
||||||
|
|
||||||
|
<div class="bg-card rounded-lg border border-border p-6">
|
||||||
|
<h4 class="font-semibold mb-3">System Health Monitoring</h4>
|
||||||
|
<p class="text-muted-foreground mb-4">Gitea Mirror includes a built-in health check endpoint at <code class="bg-muted px-1.5 py-0.5 rounded">/api/health</code> that provides:</p>
|
||||||
|
|
||||||
|
<ul class="space-y-2 mb-6">
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-primary">✓</span>
|
||||||
|
<span>System status and uptime</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-primary">✓</span>
|
||||||
|
<span>Database connectivity check</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-primary">✓</span>
|
||||||
|
<span>Memory usage statistics</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex gap-2">
|
||||||
|
<span class="text-primary">✓</span>
|
||||||
|
<span>Environment information</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="bg-muted/30 rounded-lg p-4 overflow-x-auto">
|
||||||
|
<pre class="text-sm whitespace-pre-wrap break-all"><code>{`# Basic check (returns 200 OK if healthy)
|
||||||
|
curl -I http://your-server:port/api/health
|
||||||
|
|
||||||
|
# Detailed health information (JSON)
|
||||||
|
curl http://your-server:port/api/health`}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</MainLayout>
|
||||||
@@ -1,18 +1,37 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import MainLayout from '../../layouts/main.astro';
|
import MainLayout from '../../layouts/main.astro';
|
||||||
import { LuSettings, LuRocket, LuBookOpen } from 'react-icons/lu';
|
import { LuSettings, LuRocket, LuBookOpen } from 'react-icons/lu';
|
||||||
|
|
||||||
// Helper to pick an icon based on doc.slug
|
// Define our documentation pages directly
|
||||||
// We'll use inline conditional rendering instead of this function
|
const docs = [
|
||||||
|
{
|
||||||
|
slug: 'architecture',
|
||||||
|
title: 'Architecture',
|
||||||
|
description: 'Comprehensive overview of the Gitea Mirror application architecture.',
|
||||||
|
order: 1,
|
||||||
|
icon: LuBookOpen,
|
||||||
|
href: '/docs/architecture'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'configuration',
|
||||||
|
title: 'Configuration',
|
||||||
|
description: 'Guide to configuring Gitea Mirror for your environment.',
|
||||||
|
order: 2,
|
||||||
|
icon: LuSettings,
|
||||||
|
href: '/docs/configuration'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'quickstart',
|
||||||
|
title: 'Quick Start Guide',
|
||||||
|
description: 'Get started with Gitea Mirror quickly.',
|
||||||
|
order: 3,
|
||||||
|
icon: LuRocket,
|
||||||
|
href: '/docs/quickstart'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
// Get all documentation entries, sorted by order
|
// Sort by order
|
||||||
const docs = await getCollection('docs');
|
const sortedDocs = docs.sort((a, b) => a.order - b.order);
|
||||||
const sortedDocs = docs.sort((a, b) => {
|
|
||||||
const orderA = a.data.order || 999;
|
|
||||||
const orderB = b.data.order || 999;
|
|
||||||
return orderA - orderB;
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout title="Documentation">
|
<MainLayout title="Documentation">
|
||||||
@@ -21,24 +40,25 @@ const sortedDocs = docs.sort((a, b) => {
|
|||||||
<p class="mb-10 text-lg text-muted-foreground text-center">Browse guides and technical docs for Gitea Mirror.</p>
|
<p class="mb-10 text-lg text-muted-foreground text-center">Browse guides and technical docs for Gitea Mirror.</p>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
{sortedDocs.map(doc => (
|
{sortedDocs.map(doc => {
|
||||||
<a
|
const Icon = doc.icon;
|
||||||
href={`/docs/${doc.slug}`}
|
|
||||||
class="group block p-7 border border-border rounded-2xl bg-card hover:bg-muted transition-colors shadow-lg focus:ring-2 focus:ring-ring outline-none"
|
return (
|
||||||
tabindex="0"
|
<a
|
||||||
>
|
href={doc.href}
|
||||||
<div class="flex items-center gap-3 mb-2">
|
class="group block p-7 border border-border rounded-2xl bg-card hover:bg-muted transition-colors shadow-lg focus:ring-2 focus:ring-ring outline-none"
|
||||||
<div class="w-10 h-10 bg-muted rounded-full flex items-center justify-center text-muted-foreground">
|
tabindex="0"
|
||||||
{doc.slug === 'architecture' && <LuBookOpen className="w-5 h-5" />}
|
>
|
||||||
{doc.slug === 'configuration' && <LuSettings className="w-5 h-5" />}
|
<div class="flex items-center gap-3 mb-2">
|
||||||
{doc.slug === 'quickstart' && <LuRocket className="w-5 h-5" />}
|
<div class="w-10 h-10 bg-muted rounded-full flex items-center justify-center text-muted-foreground">
|
||||||
{!['architecture', 'configuration', 'quickstart'].includes(doc.slug) && <LuBookOpen className="w-5 h-5" />}
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-semibold group-hover:text-foreground transition">{doc.title}</h2>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl font-semibold group-hover:text-foreground transition">{doc.data.title}</h2>
|
<p class="text-muted-foreground">{doc.description}</p>
|
||||||
</div>
|
</a>
|
||||||
<p class="text-muted-foreground">{doc.data.description}</p>
|
);
|
||||||
</a>
|
})}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||