mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 12:36:44 +03:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,3 +1,4 @@
|
|||||||
Evaluate all the updates being made.
|
Evaluate all the updates being made.
|
||||||
Make sure the user has clarified if its a major, minor or a patch release.
|
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.
|
Update the package.json first before you push the tag.
|
||||||
106
CHANGELOG.md
106
CHANGELOG.md
@@ -5,6 +5,112 @@ 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.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
|
## [2.13.2] - 2025-06-15
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ export async function POST({ request }: APIContext) {
|
|||||||
- Tracks status in database
|
- Tracks status in database
|
||||||
- Supports scheduled automatic mirroring
|
- 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)
|
### Database Schema (SQLite)
|
||||||
- `users` - User accounts and authentication
|
- `users` - User accounts and authentication
|
||||||
- `configs` - GitHub/Gitea connection settings
|
- `configs` - GitHub/Gitea connection settings
|
||||||
|
|||||||
60
README.md
60
README.md
@@ -36,9 +36,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)
|
||||||
@@ -317,6 +318,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
|
||||||
@@ -354,13 +406,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
|
||||||
```
|
```
|
||||||
|
|||||||
202
bun.lock
202
bun.lock
@@ -4,15 +4,18 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/mdx": "^4.3.0",
|
"@astrojs/mdx": "^4.3.0",
|
||||||
"@astrojs/node": "^9.2.2",
|
"@astrojs/node": "^9.2.2",
|
||||||
"@astrojs/react": "^4.3.0",
|
"@astrojs/react": "^4.3.0",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.25.64",
|
"zod": "^3.25.64",
|
||||||
},
|
},
|
||||||
@@ -69,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/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/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=="],
|
||||||
@@ -85,6 +93,8 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -135,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=="],
|
||||||
@@ -247,6 +271,12 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|
||||||
|
"@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/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
"@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/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=="],
|
||||||
@@ -277,6 +307,8 @@
|
|||||||
|
|
||||||
"@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=="],
|
||||||
@@ -303,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=="],
|
||||||
@@ -525,12 +559,30 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -569,6 +621,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -609,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=="],
|
||||||
@@ -695,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=="],
|
||||||
|
|
||||||
@@ -743,8 +801,16 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -759,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=="],
|
||||||
@@ -767,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=="],
|
||||||
@@ -835,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=="],
|
||||||
@@ -857,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=="],
|
||||||
@@ -959,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=="],
|
||||||
@@ -1029,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=="],
|
||||||
@@ -1045,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=="],
|
||||||
@@ -1091,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=="],
|
||||||
@@ -1101,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=="],
|
||||||
@@ -1111,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=="],
|
||||||
@@ -1171,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=="],
|
||||||
@@ -1183,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=="],
|
||||||
@@ -1229,11 +1331,11 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
@@ -1273,6 +1375,8 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -1293,8 +1397,12 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
@@ -1359,6 +1467,40 @@
|
|||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||||
@@ -1387,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=="],
|
||||||
@@ -1447,16 +1597,20 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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=="],
|
||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
@@ -1465,12 +1619,24 @@
|
|||||||
|
|
||||||
"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=="],
|
"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/@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=="],
|
"@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
||||||
@@ -1481,14 +1647,24 @@
|
|||||||
|
|
||||||
"@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=="],
|
"@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/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/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||||
@@ -1496,5 +1672,13 @@
|
|||||||
"@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/@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=="],
|
"@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:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.13.2",
|
"version": "2.18.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
@@ -31,15 +31,18 @@
|
|||||||
"astro": "bunx --bun astro"
|
"astro": "bunx --bun astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/mdx": "^4.3.0",
|
"@astrojs/mdx": "^4.3.0",
|
||||||
"@astrojs/node": "^9.2.2",
|
"@astrojs/node": "^9.2.2",
|
||||||
"@astrojs/react": "^4.3.0",
|
"@astrojs/react": "^4.3.0",
|
||||||
"@octokit/rest": "^22.0.0",
|
"@octokit/rest": "^22.0.0",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
"@radix-ui/react-popover": "^1.1.14",
|
||||||
"@radix-ui/react-radio-group": "^1.3.7",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.10",
|
"tailwindcss": "^4.1.10",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.3.4",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"zod": "^3.25.64"
|
"zod": "^3.25.64"
|
||||||
},
|
},
|
||||||
|
|||||||
56
scripts/README-dev.md
Normal file
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.
|
||||||
68
scripts/gitea-app.ini
Normal file
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
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
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
|
||||||
81
src/components/NotFound.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -97,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
|
||||||
|
|
||||||
@@ -571,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 || !isConfigFormValid()}
|
disabled={isSyncing || !isGitHubConfigValid()}
|
||||||
title={
|
title={
|
||||||
!isConfigFormValid()
|
!isGitHubConfigValid()
|
||||||
? 'Please fill all required GitHub and Gitea fields'
|
? 'Please fill GitHub username and token fields'
|
||||||
: isSyncing
|
: isSyncing
|
||||||
? 'Import in progress'
|
? 'Import in progress'
|
||||||
: 'Import GitHub Data'
|
: 'Import GitHub Data'
|
||||||
@@ -598,7 +603,7 @@ export function ConfigTabs() {
|
|||||||
{/* Content section - Grid layout */}
|
{/* Content section - Grid layout */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* GitHub & Gitea connections - Side by side */}
|
{/* GitHub & Gitea connections - Side by side */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<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 =>
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { GitHubConfigForm } from './GitHubConfigForm';
|
|
||||||
import { GiteaConfigForm } from './GiteaConfigForm';
|
|
||||||
import { Separator } from '../ui/separator';
|
|
||||||
import type { GitHubConfig, GiteaConfig } from '@/types/config';
|
|
||||||
|
|
||||||
interface ConnectionsFormProps {
|
|
||||||
githubConfig: GitHubConfig;
|
|
||||||
giteaConfig: GiteaConfig;
|
|
||||||
setGithubConfig: (update: GitHubConfig | ((prev: GitHubConfig) => GitHubConfig)) => void;
|
|
||||||
setGiteaConfig: (update: GiteaConfig | ((prev: GiteaConfig) => GiteaConfig)) => void;
|
|
||||||
onAutoSaveGitHub?: (config: GitHubConfig) => Promise<void>;
|
|
||||||
onAutoSaveGitea?: (config: GiteaConfig) => Promise<void>;
|
|
||||||
isAutoSavingGitHub?: boolean;
|
|
||||||
isAutoSavingGitea?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConnectionsForm({
|
|
||||||
githubConfig,
|
|
||||||
giteaConfig,
|
|
||||||
setGithubConfig,
|
|
||||||
setGiteaConfig,
|
|
||||||
onAutoSaveGitHub,
|
|
||||||
onAutoSaveGitea,
|
|
||||||
isAutoSavingGitHub,
|
|
||||||
isAutoSavingGitea,
|
|
||||||
}: ConnectionsFormProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<GitHubConfigForm
|
|
||||||
config={githubConfig}
|
|
||||||
setConfig={setGithubConfig}
|
|
||||||
onAutoSave={onAutoSaveGitHub}
|
|
||||||
isAutoSaving={isAutoSavingGitHub}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<GiteaConfigForm
|
|
||||||
config={giteaConfig}
|
|
||||||
setConfig={setGiteaConfig}
|
|
||||||
onAutoSave={onAutoSaveGitea}
|
|
||||||
isAutoSaving={isAutoSavingGitea}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
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, MirrorOptions, AdvancedOptions } 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 { GitHubMirrorSettings } from "./GitHubMirrorSettings";
|
||||||
import { Separator } from "../ui/separator";
|
import { Separator } from "../ui/separator";
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from "@/components/ui/hover-card";
|
||||||
|
|
||||||
interface GitHubConfigFormProps {
|
interface GitHubConfigFormProps {
|
||||||
config: GitHubConfig;
|
config: GitHubConfig;
|
||||||
@@ -87,7 +87,7 @@ export function GitHubConfigForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full self-start">
|
<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
|
||||||
@@ -102,7 +102,7 @@ export function GitHubConfigForm({
|
|||||||
</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"
|
||||||
@@ -123,12 +123,49 @@ export function GitHubConfigForm({
|
|||||||
</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"
|
||||||
@@ -136,7 +173,7 @@ export function GitHubConfigForm({
|
|||||||
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
|
||||||
@@ -163,45 +200,8 @@ export function GitHubConfigForm({
|
|||||||
if (onAdvancedOptionsAutoSave) onAdvancedOptionsAutoSave(newOptions);
|
if (onAdvancedOptionsAutoSave) onAdvancedOptionsAutoSave(newOptions);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,22 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import {
|
import {
|
||||||
Info,
|
Info,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Star,
|
Star,
|
||||||
Building2,
|
|
||||||
Lock,
|
Lock,
|
||||||
Archive,
|
Archive,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
@@ -22,7 +27,9 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
Target,
|
Target,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
GitFork
|
GitFork,
|
||||||
|
ChevronDown,
|
||||||
|
Funnel
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { GitHubConfig, MirrorOptions, AdvancedOptions } from "@/types/config";
|
import type { GitHubConfig, MirrorOptions, AdvancedOptions } from "@/types/config";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -70,6 +77,18 @@ export function GitHubMirrorSettings({
|
|||||||
// When metadata is disabled, all components should be disabled
|
// When metadata is disabled, all components should be disabled
|
||||||
const isMetadataEnabled = mirrorOptions.mirrorMetadata;
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Repository Selection Section */}
|
{/* Repository Selection Section */}
|
||||||
@@ -100,28 +119,162 @@ export function GitHubMirrorSettings({
|
|||||||
Include private repositories
|
Include private repositories
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Mirror your private repositories (requires appropriate token permissions)
|
Mirror your private repositories
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<Checkbox
|
<div className="flex items-start space-x-3">
|
||||||
id="starred-repos"
|
<Checkbox
|
||||||
checked={githubConfig.mirrorStarred}
|
id="starred-repos"
|
||||||
onCheckedChange={(checked) => handleGitHubChange('mirrorStarred', !!checked)}
|
checked={githubConfig.mirrorStarred}
|
||||||
/>
|
onCheckedChange={(checked) => handleGitHubChange('mirrorStarred', !!checked)}
|
||||||
<div className="space-y-0.5 flex-1">
|
/>
|
||||||
<Label
|
<div className="space-y-0.5 flex-1">
|
||||||
htmlFor="starred-repos"
|
<Label
|
||||||
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
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
|
<Star className="h-3.5 w-3.5" />
|
||||||
</Label>
|
Mirror starred repositories
|
||||||
<p className="text-xs text-muted-foreground">
|
</Label>
|
||||||
Include repositories you've starred on GitHub
|
<p className="text-xs text-muted-foreground">
|
||||||
</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,7 +325,7 @@ export function GitHubMirrorSettings({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="mirror-metadata"
|
id="mirror-metadata"
|
||||||
@@ -193,92 +346,142 @@ export function GitHubMirrorSettings({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metadata sub-options */}
|
{/* Metadata multi-select - inline to prevent layout shift */}
|
||||||
{mirrorOptions.mirrorMetadata && (
|
<div className={cn(
|
||||||
<div className="ml-7 space-y-2 p-3 bg-muted/30 dark:bg-muted/10 rounded-md">
|
"flex items-center justify-end transition-opacity duration-200",
|
||||||
<div className="grid grid-cols-2 gap-3">
|
mirrorOptions.mirrorMetadata ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||||
<div className="flex items-center space-x-2">
|
)}>
|
||||||
<Checkbox
|
<Popover>
|
||||||
id="metadata-issues"
|
<PopoverTrigger asChild>
|
||||||
checked={mirrorOptions.metadataComponents.issues}
|
<Button
|
||||||
onCheckedChange={(checked) => handleMetadataComponentChange('issues', !!checked)}
|
variant="outline"
|
||||||
disabled={!isMetadataEnabled}
|
size="sm"
|
||||||
/>
|
disabled={!mirrorOptions.mirrorMetadata}
|
||||||
<Label
|
className="h-8 text-xs font-normal min-w-[140px] justify-between"
|
||||||
htmlFor="metadata-issues"
|
>
|
||||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
<span>
|
||||||
>
|
{(() => {
|
||||||
<MessageSquare className="h-3 w-3" />
|
const selectedCount = Object.values(mirrorOptions.metadataComponents).filter(Boolean).length;
|
||||||
Issues
|
const totalCount = Object.keys(mirrorOptions.metadataComponents).length;
|
||||||
</Label>
|
if (selectedCount === 0) return "No items selected";
|
||||||
</div>
|
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;
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
// Update all metadata components at once
|
||||||
<Checkbox
|
onMirrorOptionsChange({
|
||||||
id="metadata-prs"
|
...mirrorOptions,
|
||||||
checked={mirrorOptions.metadataComponents.pullRequests}
|
metadataComponents: {
|
||||||
onCheckedChange={(checked) => handleMetadataComponentChange('pullRequests', !!checked)}
|
issues: newValue,
|
||||||
disabled={!isMetadataEnabled}
|
pullRequests: newValue,
|
||||||
/>
|
labels: newValue,
|
||||||
<Label
|
milestones: newValue,
|
||||||
htmlFor="metadata-prs"
|
wiki: newValue,
|
||||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
},
|
||||||
>
|
});
|
||||||
<GitPullRequest className="h-3 w-3" />
|
}}
|
||||||
Pull Requests
|
>
|
||||||
</Label>
|
{Object.values(mirrorOptions.metadataComponents).every(Boolean) ? 'Deselect all' : 'Select all'}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<Separator className="my-2" />
|
||||||
<Checkbox
|
|
||||||
id="metadata-labels"
|
|
||||||
checked={mirrorOptions.metadataComponents.labels}
|
|
||||||
onCheckedChange={(checked) => handleMetadataComponentChange('labels', !!checked)}
|
|
||||||
disabled={!isMetadataEnabled}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
htmlFor="metadata-labels"
|
|
||||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<Tag className="h-3 w-3" />
|
|
||||||
Labels
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="space-y-2">
|
||||||
<Checkbox
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
id="metadata-milestones"
|
<Checkbox
|
||||||
checked={mirrorOptions.metadataComponents.milestones}
|
id="metadata-issues-popup"
|
||||||
onCheckedChange={(checked) => handleMetadataComponentChange('milestones', !!checked)}
|
checked={mirrorOptions.metadataComponents.issues}
|
||||||
disabled={!isMetadataEnabled}
|
onCheckedChange={(checked) => handleMetadataComponentChange('issues', !!checked)}
|
||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor="metadata-milestones"
|
htmlFor="metadata-issues-popup"
|
||||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||||
>
|
>
|
||||||
<Target className="h-3 w-3" />
|
<MessageSquare className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
Milestones
|
Issues
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="metadata-wiki"
|
id="metadata-prs-popup"
|
||||||
checked={mirrorOptions.metadataComponents.wiki}
|
checked={mirrorOptions.metadataComponents.pullRequests}
|
||||||
onCheckedChange={(checked) => handleMetadataComponentChange('wiki', !!checked)}
|
onCheckedChange={(checked) => handleMetadataComponentChange('pullRequests', !!checked)}
|
||||||
disabled={!isMetadataEnabled}
|
/>
|
||||||
/>
|
<Label
|
||||||
<Label
|
htmlFor="metadata-prs-popup"
|
||||||
htmlFor="metadata-wiki"
|
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
>
|
||||||
>
|
<GitPullRequest className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
<BookOpen className="h-3 w-3" />
|
Pull Requests
|
||||||
Wiki
|
</Label>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</PopoverContent>
|
||||||
</div>
|
</Popover>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,7 +492,7 @@ export function GitHubMirrorSettings({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
<Building2 className="h-4 w-4" />
|
<Funnel className="h-4 w-4" />
|
||||||
Filtering & Behavior
|
Filtering & Behavior
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-xs text-muted-foreground mb-4">
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
@@ -317,42 +520,6 @@ export function GitHubMirrorSettings({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{githubConfig.mirrorStarred && (
|
|
||||||
<div className="flex items-start space-x-3">
|
|
||||||
<Checkbox
|
|
||||||
id="skip-starred-metadata"
|
|
||||||
checked={advancedOptions.skipStarredIssues}
|
|
||||||
onCheckedChange={(checked) => handleAdvancedChange('skipStarredIssues', !!checked)}
|
|
||||||
/>
|
|
||||||
<div className="space-y-0.5 flex-1">
|
|
||||||
<Label
|
|
||||||
htmlFor="skip-starred-metadata"
|
|
||||||
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<Star className="h-3.5 w-3.5" />
|
|
||||||
Lightweight starred repository mirroring
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Info className="h-3 w-3 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right" className="max-w-xs">
|
|
||||||
<p className="text-xs">
|
|
||||||
When enabled, starred repositories will only mirror code,
|
|
||||||
skipping issues, PRs, and other metadata to reduce storage
|
|
||||||
and improve performance.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</Label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Only mirror code from starred repos, skip issues and metadata
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,24 +3,14 @@ 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 { Checkbox } from "../ui/checkbox";
|
|
||||||
import { giteaApi } from "@/lib/api";
|
import { giteaApi } from "@/lib/api";
|
||||||
import type { GiteaConfig, GiteaOrgVisibility, MirrorStrategy } from "@/types/config";
|
import type { GiteaConfig, MirrorStrategy } from "@/types/config";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Info } from "lucide-react";
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|
||||||
import { OrganizationStrategy } from "./OrganizationStrategy";
|
import { OrganizationStrategy } from "./OrganizationStrategy";
|
||||||
|
import { OrganizationConfiguration } from "./OrganizationConfiguration";
|
||||||
import { Separator } from "../ui/separator";
|
import { Separator } from "../ui/separator";
|
||||||
|
|
||||||
interface GiteaConfigFormProps {
|
interface GiteaConfigFormProps {
|
||||||
@@ -133,7 +123,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full self-start">
|
<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
|
||||||
@@ -148,7 +138,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
</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"
|
||||||
@@ -210,13 +200,25 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator className="my-2" />
|
<Separator />
|
||||||
|
|
||||||
<OrganizationStrategy
|
<OrganizationStrategy
|
||||||
strategy={mirrorStrategy}
|
strategy={mirrorStrategy}
|
||||||
destinationOrg={config.organization}
|
destinationOrg={config.organization}
|
||||||
starredReposOrg={config.starredReposOrg}
|
starredReposOrg={config.starredReposOrg}
|
||||||
onStrategyChange={setMirrorStrategy}
|
onStrategyChange={setMirrorStrategy}
|
||||||
|
githubUsername={githubUsername}
|
||||||
|
giteaUsername={config.username}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<OrganizationConfiguration
|
||||||
|
strategy={mirrorStrategy}
|
||||||
|
destinationOrg={config.organization}
|
||||||
|
starredReposOrg={config.starredReposOrg}
|
||||||
|
personalReposOrg={config.personalReposOrg}
|
||||||
|
visibility={config.visibility}
|
||||||
onDestinationOrgChange={(org) => {
|
onDestinationOrgChange={(org) => {
|
||||||
const newConfig = { ...config, organization: org };
|
const newConfig = { ...config, organization: org };
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
@@ -227,54 +229,18 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
if (onAutoSave) onAutoSave(newConfig);
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
}}
|
}}
|
||||||
githubUsername={githubUsername}
|
onPersonalReposOrgChange={(org) => {
|
||||||
giteaUsername={config.username}
|
const newConfig = { ...config, personalReposOrg: org };
|
||||||
|
setConfig(newConfig);
|
||||||
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
|
}}
|
||||||
|
onVisibilityChange={(visibility) => {
|
||||||
|
const newConfig = { ...config, visibility };
|
||||||
|
setConfig(newConfig);
|
||||||
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="visibility"
|
|
||||||
className="block text-sm font-medium mb-1.5"
|
|
||||||
>
|
|
||||||
Organization Visibility
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
name="visibility"
|
|
||||||
value={config.visibility}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleChange({
|
|
||||||
target: { name: "visibility", value },
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Visibility for newly created organizations
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="">
|
|
||||||
{/* Footer content can be added here if needed */}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
201
src/components/config/OrganizationConfiguration.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -1,250 +1,274 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Info, GitBranch, FolderTree, Star, Building2, User, Building } from "lucide-react";
|
||||||
import { Info, GitBranch, FolderTree, Package, Star, Building2, User, ChevronDown, ChevronUp } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
HoverCard,
|
||||||
TooltipContent,
|
HoverCardContent,
|
||||||
TooltipProvider,
|
HoverCardTrigger,
|
||||||
TooltipTrigger,
|
} from "@/components/ui/hover-card";
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "@/components/ui/collapsible";
|
|
||||||
|
|
||||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user";
|
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
|
||||||
interface OrganizationStrategyProps {
|
interface OrganizationStrategyProps {
|
||||||
strategy: MirrorStrategy;
|
strategy: MirrorStrategy;
|
||||||
destinationOrg?: string;
|
destinationOrg?: string;
|
||||||
starredReposOrg?: string;
|
starredReposOrg?: string;
|
||||||
onStrategyChange: (strategy: MirrorStrategy) => void;
|
onStrategyChange: (strategy: MirrorStrategy) => void;
|
||||||
onDestinationOrgChange: (org: string) => void;
|
|
||||||
onStarredReposOrgChange: (org: string) => void;
|
|
||||||
githubUsername?: string;
|
githubUsername?: string;
|
||||||
giteaUsername?: string;
|
giteaUsername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const strategyConfig = {
|
const strategyConfig = {
|
||||||
preserve: {
|
preserve: {
|
||||||
title: "Mirror GitHub Structure",
|
title: "Preserve Structure",
|
||||||
icon: FolderTree,
|
icon: FolderTree,
|
||||||
description: "Keep the same organization structure as GitHub",
|
description: "Keep the exact same organization structure as GitHub",
|
||||||
color: "text-blue-600 dark:text-blue-400",
|
color: "text-blue-600 dark:text-blue-400",
|
||||||
bgColor: "bg-blue-50 dark:bg-blue-950/20",
|
bgColor: "bg-blue-50 dark:bg-blue-950/20",
|
||||||
borderColor: "border-blue-200 dark:border-blue-900",
|
borderColor: "border-blue-200 dark:border-blue-900",
|
||||||
details: [
|
repoColors: {
|
||||||
"Personal repos → Your Gitea username",
|
bg: "bg-blue-50 dark:bg-blue-950/30",
|
||||||
"Org repos → Same org name in Gitea",
|
icon: "text-blue-600 dark:text-blue-400"
|
||||||
"Team structure preserved"
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"single-org": {
|
"single-org": {
|
||||||
title: "Consolidate to One Org",
|
title: "Single Organization",
|
||||||
icon: Building2,
|
icon: Building2,
|
||||||
description: "Mirror all repositories into a single organization",
|
description: "Consolidate all repositories into one Gitea organization",
|
||||||
color: "text-purple-600 dark:text-purple-400",
|
color: "text-purple-600 dark:text-purple-400",
|
||||||
bgColor: "bg-purple-50 dark:bg-purple-950/20",
|
bgColor: "bg-purple-50 dark:bg-purple-950/20",
|
||||||
borderColor: "border-purple-200 dark:border-purple-900",
|
borderColor: "border-purple-200 dark:border-purple-900",
|
||||||
details: [
|
repoColors: {
|
||||||
"All repos in one place",
|
bg: "bg-purple-50 dark:bg-purple-950/30",
|
||||||
"Simplified management",
|
icon: "text-purple-600 dark:text-purple-400"
|
||||||
"Custom organization name"
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"flat-user": {
|
"flat-user": {
|
||||||
title: "Flat User Structure",
|
title: "User Repositories",
|
||||||
icon: User,
|
icon: User,
|
||||||
description: "Mirror all repositories under your user account",
|
description: "Place all repositories directly under your user account",
|
||||||
color: "text-green-600 dark:text-green-400",
|
color: "text-green-600 dark:text-green-400",
|
||||||
bgColor: "bg-green-50 dark:bg-green-950/20",
|
bgColor: "bg-green-50 dark:bg-green-950/20",
|
||||||
borderColor: "border-green-200 dark:border-green-900",
|
borderColor: "border-green-200 dark:border-green-900",
|
||||||
details: [
|
repoColors: {
|
||||||
"All repos under your username",
|
bg: "bg-green-50 dark:bg-green-950/30",
|
||||||
"No organizations needed",
|
icon: "text-green-600 dark:text-green-400"
|
||||||
"Simple and personal"
|
}
|
||||||
]
|
},
|
||||||
|
"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 StrategyVisualizer: React.FC<{
|
const MappingPreview: React.FC<{
|
||||||
strategy: MirrorStrategy;
|
strategy: MirrorStrategy;
|
||||||
|
config: typeof strategyConfig.preserve;
|
||||||
destinationOrg?: string;
|
destinationOrg?: string;
|
||||||
starredReposOrg?: string;
|
starredReposOrg?: string;
|
||||||
githubUsername?: string;
|
githubUsername?: string;
|
||||||
giteaUsername?: string;
|
giteaUsername?: string;
|
||||||
}> = ({ strategy, destinationOrg, starredReposOrg, githubUsername, giteaUsername }) => {
|
}> = ({ strategy, config, destinationOrg, starredReposOrg, githubUsername, giteaUsername }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const displayGithubUsername = githubUsername || "<username>";
|
const displayGithubUsername = githubUsername || "<username>";
|
||||||
const displayGiteaUsername = giteaUsername || "<username>";
|
const displayGiteaUsername = giteaUsername || "<username>";
|
||||||
const isGithubPlaceholder = !githubUsername;
|
const isGithubPlaceholder = !githubUsername;
|
||||||
const isGiteaPlaceholder = !giteaUsername;
|
const isGiteaPlaceholder = !giteaUsername;
|
||||||
const renderPreserveStructure = () => (
|
|
||||||
<div className="flex items-center justify-between gap-8 p-6">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
<span className={cn("text-sm", isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<Building2 className="h-4 w-4" />
|
|
||||||
<span className="text-sm">my-org/team-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<Star className="h-4 w-4" />
|
|
||||||
<span className="text-sm">awesome/starred-repo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
if (strategy === "preserve") {
|
||||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
return (
|
||||||
</div>
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
<div className="flex-1">
|
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
<div className="space-y-1.5">
|
||||||
<div className="space-y-2">
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
<div className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
|
<User className="h-3 w-3" />
|
||||||
<User className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||||
<span className={cn("text-sm", isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
|
|
||||||
<Building2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<span className="text-sm">my-org/team-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
|
|
||||||
<Building2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSingleOrg = () => (
|
|
||||||
<div className="flex items-center justify-between gap-8 p-6">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
<span className={cn("text-sm", isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<Building2 className="h-4 w-4" />
|
|
||||||
<span className="text-sm">my-org/team-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<Star className="h-4 w-4" />
|
|
||||||
<span className="text-sm">awesome/starred-repo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-purple-50 dark:bg-purple-950/30 rounded">
|
|
||||||
<Building2 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
|
||||||
<span className="text-sm">{destinationOrg || "github-mirrors"}/my-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-purple-50 dark:bg-purple-950/30 rounded">
|
|
||||||
<Building2 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
|
||||||
<span className="text-sm">{destinationOrg || "github-mirrors"}/team-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-purple-50 dark:bg-purple-950/30 rounded">
|
|
||||||
<Building2 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
|
||||||
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderFlatUser = () => (
|
|
||||||
<div className="flex items-center justify-between gap-8 p-6">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<User className="h-4 w-4" />
|
|
||||||
<span className={cn("text-sm", isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<Building2 className="h-4 w-4" />
|
|
||||||
<span className="text-sm">my-org/team-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
|
||||||
<Star className="h-4 w-4" />
|
|
||||||
<span className="text-sm">awesome/starred-repo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-950/30 rounded">
|
|
||||||
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
|
||||||
<span className={cn("text-sm", isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-950/30 rounded">
|
|
||||||
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
|
||||||
<span className={cn("text-sm", isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/team-repo</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-950/30 rounded">
|
|
||||||
<Building2 className="h-4 w-4 text-green-600 dark:text-green-400" />
|
|
||||||
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-4">
|
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<Card className="overflow-hidden">
|
|
||||||
<CollapsibleTrigger className="w-full">
|
|
||||||
<div className="bg-muted/50 p-3 border-b hover:bg-muted/70 transition-colors cursor-pointer">
|
|
||||||
<h4 className="text-sm font-medium flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<Package className="h-4 w-4" />
|
|
||||||
Repository Mapping Preview
|
|
||||||
</span>
|
|
||||||
{isOpen ? (
|
|
||||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</h4>
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTrigger>
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
<CollapsibleContent>
|
<Building2 className="h-3 w-3" />
|
||||||
{strategy === "preserve" && renderPreserveStructure()}
|
<span>my-org/team-repo</span>
|
||||||
{strategy === "single-org" && renderSingleOrg()}
|
</div>
|
||||||
{strategy === "flat-user" && renderFlatUser()}
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
</CollapsibleContent>
|
<Star className="h-3 w-3" />
|
||||||
</Card>
|
<span>awesome/starred-repo</span>
|
||||||
</Collapsible>
|
</div>
|
||||||
</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> = ({
|
export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||||
@@ -252,23 +276,87 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
|||||||
destinationOrg,
|
destinationOrg,
|
||||||
starredReposOrg,
|
starredReposOrg,
|
||||||
onStrategyChange,
|
onStrategyChange,
|
||||||
onDestinationOrgChange,
|
|
||||||
onStarredReposOrgChange,
|
|
||||||
githubUsername,
|
githubUsername,
|
||||||
giteaUsername,
|
giteaUsername,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
<div>
|
<div className="flex items-start justify-between gap-4">
|
||||||
<h3 className="text-lg font-semibold mb-1">Organization Strategy</h3>
|
<div className="flex-1">
|
||||||
<p className="text-sm text-muted-foreground">
|
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||||
Choose how your repositories will be organized in Gitea
|
<Building className="h-4 w-4" />
|
||||||
</p>
|
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>
|
</div>
|
||||||
|
|
||||||
<RadioGroup value={strategy} onValueChange={onStrategyChange}>
|
<RadioGroup value={strategy} onValueChange={onStrategyChange}>
|
||||||
<div className="grid gap-4">
|
<div className="grid grid-cols-1 2xl:grid-cols-2 gap-4">
|
||||||
{(Object.entries(strategyConfig) as [MirrorStrategy, typeof strategyConfig.preserve][]).map(([key, config]) => {
|
{(Object.entries(strategyConfig) as [MirrorStrategy, typeof strategyConfig.preserve][]).map(([key, config]) => {
|
||||||
const isSelected = strategy === key;
|
const isSelected = strategy === key;
|
||||||
const Icon = config.icon;
|
const Icon = config.icon;
|
||||||
@@ -283,12 +371,11 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
|||||||
!isSelected && "border-muted"
|
!isSelected && "border-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<div className="p-3">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
value={key}
|
value={key}
|
||||||
id={key}
|
id={key}
|
||||||
className="mt-1"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@@ -296,36 +383,43 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
|||||||
isSelected ? config.bgColor : "bg-muted dark:bg-muted/50"
|
isSelected ? config.bgColor : "bg-muted dark:bg-muted/50"
|
||||||
)}>
|
)}>
|
||||||
<Icon className={cn(
|
<Icon className={cn(
|
||||||
"h-5 w-5",
|
"h-4 w-4",
|
||||||
isSelected ? config.color : "text-muted-foreground dark:text-muted-foreground/70"
|
isSelected ? config.color : "text-muted-foreground dark:text-muted-foreground/70"
|
||||||
)} />
|
)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2">
|
||||||
<h4 className="font-medium">{config.title}</h4>
|
<h4 className="font-medium text-sm">{config.title}</h4>
|
||||||
{isSelected && (
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
Selected
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{config.description}
|
{config.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
{config.details.map((detail, idx) => (
|
|
||||||
<div key={idx} className="flex items-center gap-2">
|
|
||||||
<div className={cn(
|
|
||||||
"h-1.5 w-1.5 rounded-full",
|
|
||||||
isSelected ? config.bgColor : "bg-muted dark:bg-muted/50"
|
|
||||||
)} />
|
|
||||||
<span className="text-xs text-muted-foreground">{detail}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -335,76 +429,6 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
{strategy === "single-org" && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Card className="p-4 border-purple-200 dark:border-purple-900 bg-purple-50/50 dark:bg-purple-950/20">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="destinationOrg" className="flex items-center gap-2">
|
|
||||||
Destination Organization
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>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="mt-1.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card className="p-4 border-orange-200 dark:border-orange-900 bg-orange-50/50 dark:bg-orange-950/20">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="starredReposOrg" className="flex items-center gap-2">
|
|
||||||
<Star className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
|
||||||
Starred Repositories 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="mt-1.5"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground dark:text-muted-foreground/70 mt-1">
|
|
||||||
Keep starred repos organized separately from your own repositories
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<StrategyVisualizer
|
|
||||||
strategy={strategy}
|
|
||||||
destinationOrg={destinationOrg}
|
|
||||||
starredReposOrg={starredReposOrg}
|
|
||||||
githubUsername={githubUsername}
|
|
||||||
giteaUsername={giteaUsername}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
193
src/components/organizations/MirrorDestinationEditor.tsx
Normal file
193
src/components/organizations/MirrorDestinationEditor.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ArrowRight, Edit3, RotateCcw, CheckCircle2, XCircle, 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -404,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,29 +164,56 @@ 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>
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground mb-4">
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
@@ -125,78 +223,147 @@ export function OrganizationList({
|
|||||||
{org.repositoryCount === 1 ? "repository" : "repositories"}
|
{org.repositoryCount === 1 ? "repository" : "repositories"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{(org.publicRepositoryCount !== undefined ||
|
{/* Always render this section to prevent layout shift */}
|
||||||
org.privateRepositoryCount !== undefined ||
|
<div className="flex gap-4 mt-2 text-xs min-h-[20px]">
|
||||||
org.forkRepositoryCount !== undefined) && (
|
{isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? (
|
||||||
<div className="flex gap-4 mt-2 text-xs">
|
<>
|
||||||
{org.publicRepositoryCount !== undefined && (
|
<Skeleton className="h-3 w-16" />
|
||||||
<span className="flex items-center gap-1">
|
<Skeleton className="h-3 w-16" />
|
||||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
</>
|
||||||
{org.publicRepositoryCount} public
|
) : (
|
||||||
</span>
|
<>
|
||||||
)}
|
{org.publicRepositoryCount !== undefined ? (
|
||||||
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 && (
|
<span className="flex items-center gap-1">
|
||||||
<span className="flex items-center gap-1">
|
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||||
<div className="h-2 w-2 rounded-full bg-orange-500" />
|
{org.publicRepositoryCount} public
|
||||||
{org.privateRepositoryCount} private
|
</span>
|
||||||
</span>
|
) : null}
|
||||||
)}
|
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 ? (
|
||||||
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 && (
|
<span className="flex items-center gap-1">
|
||||||
<span className="flex items-center gap-1">
|
<div className="h-2 w-2 rounded-full bg-orange-500" />
|
||||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
{org.privateRepositoryCount} private
|
||||||
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''}
|
</span>
|
||||||
</span>
|
) : null}
|
||||||
)}
|
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 ? (
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
Enable mirroring
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{isLoading && (
|
{org.status === "mirroring" && (
|
||||||
<RefreshCw className="opacity-50 h-4 w-4 animate-spin ml-4" />
|
<Button size="sm" disabled variant="outline">
|
||||||
|
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||||
|
Mirroring...
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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
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>
|
||||||
@@ -263,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 */}
|
||||||
@@ -277,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 */}
|
||||||
|
|||||||
42
src/components/ui/hover-card.tsx
Normal file
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 }
|
||||||
@@ -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,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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -28,11 +28,44 @@ try {
|
|||||||
|
|
||||||
// Ensure all required tables exist
|
// Ensure all required tables exist
|
||||||
ensureTablesExist(sqlite);
|
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
|
* Ensure all required tables exist in the database
|
||||||
*/
|
*/
|
||||||
@@ -159,6 +192,7 @@ function createTable(db: Database, tableName: string) {
|
|||||||
last_mirrored INTEGER,
|
last_mirrored INTEGER,
|
||||||
error_message TEXT,
|
error_message TEXT,
|
||||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
destination_org TEXT,
|
||||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||||
updated_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 (user_id) REFERENCES users(id),
|
||||||
@@ -437,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"),
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ export const configSchema = z.object({
|
|||||||
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),
|
preserveOrgStructure: z.boolean().default(false),
|
||||||
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).optional(),
|
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([]),
|
||||||
@@ -100,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()),
|
||||||
@@ -158,6 +160,9 @@ export const organizationSchema = z.object({
|
|||||||
privateRepositoryCount: z.number().optional(),
|
privateRepositoryCount: z.number().optional(),
|
||||||
forkRepositoryCount: 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));
|
||||||
@@ -291,3 +292,133 @@ describe("Gitea Repository Mirroring", () => {
|
|||||||
expect(mockGetOrCreateGiteaOrg).toHaveBeenCalled();
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
240
src/lib/gitea.ts
240
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 ({
|
||||||
@@ -134,15 +254,18 @@ 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. Updating database status.`
|
`Repository ${repository.name} already exists in Gitea under ${repoOwner}. Updating database status.`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update database to reflect that the repository is already mirrored
|
// Update database to reflect that the repository is already mirrored
|
||||||
@@ -153,7 +276,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!));
|
||||||
|
|
||||||
@@ -163,7 +286,7 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
repositoryId: repository.id,
|
repositoryId: repository.id,
|
||||||
repositoryName: repository.name,
|
repositoryName: repository.name,
|
||||||
message: `Repository ${repository.name} already exists in Gitea`,
|
message: `Repository ${repository.name} already exists in Gitea`,
|
||||||
details: `Repository ${repository.name} was found to already exist in Gitea and database status was updated.`,
|
details: `Repository ${repository.name} was found to already exist in Gitea under ${repoOwner} and database status was updated.`,
|
||||||
status: "mirrored",
|
status: "mirrored",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -212,6 +335,15 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
|
|
||||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||||
|
|
||||||
|
// Handle organization creation if needed for single-org or preserve strategies
|
||||||
|
if (repoOwner !== config.giteaConfig.username && !repository.isStarred) {
|
||||||
|
// Need to create the organization if it doesn't exist
|
||||||
|
await getOrCreateGiteaOrg({
|
||||||
|
orgName: repoOwner,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const response = await httpPost(
|
const response = await httpPost(
|
||||||
apiUrl,
|
apiUrl,
|
||||||
{
|
{
|
||||||
@@ -220,7 +352,7 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
mirror: true,
|
mirror: true,
|
||||||
wiki: config.githubConfig.mirrorWiki || false, // will mirror wiki if it exists
|
wiki: config.githubConfig.mirrorWiki || false, // will mirror wiki if it exists
|
||||||
private: repository.isPrivate,
|
private: repository.isPrivate,
|
||||||
repo_owner: config.giteaConfig.username,
|
repo_owner: repoOwner,
|
||||||
description: "",
|
description: "",
|
||||||
service: "git",
|
service: "git",
|
||||||
},
|
},
|
||||||
@@ -237,7 +369,11 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -256,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!));
|
||||||
|
|
||||||
@@ -390,7 +526,7 @@ 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",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -582,7 +718,11 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -729,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
|
||||||
@@ -771,17 +937,27 @@ export async function mirrorGitHubOrgToGitea({
|
|||||||
|
|
||||||
// Log the start of mirroring
|
// Log the start of mirroring
|
||||||
console.log(
|
console.log(
|
||||||
`Starting mirror for repository: ${repo.name} in organization ${organization.name}`
|
`Starting mirror for repository: ${repo.name} from GitHub org ${organization.name}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mirror the repository
|
// Mirror the repository based on strategy
|
||||||
await mirrorGitHubRepoToGiteaOrg({
|
if (mirrorStrategy === "flat-user") {
|
||||||
octokit,
|
// For flat-user strategy, mirror directly to user account
|
||||||
config,
|
await mirrorGithubRepoToGitea({
|
||||||
repository: repoData,
|
octokit,
|
||||||
giteaOrgId,
|
repository: repoData,
|
||||||
orgName: organization.name,
|
config,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// For preserve and single-org strategies, use organization
|
||||||
|
await mirrorGitHubRepoToGiteaOrg({
|
||||||
|
octokit,
|
||||||
|
config,
|
||||||
|
repository: repoData,
|
||||||
|
giteaOrgId: giteaOrgId!,
|
||||||
|
orgName: targetOrgName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return repo;
|
return repo;
|
||||||
},
|
},
|
||||||
@@ -905,8 +1081,8 @@ 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({
|
||||||
@@ -1207,7 +1383,7 @@ export async function mirrorGitHubReleasesToGitea({
|
|||||||
throw new Error("Gitea config is incomplete for mirroring releases.");
|
throw new Error("Gitea config is incomplete for mirroring releases.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const repoOwner = getGiteaRepoOwner({
|
const repoOwner = await getGiteaRepoOwnerAsync({
|
||||||
config,
|
config,
|
||||||
repository,
|
repository,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ interface DbGiteaConfig {
|
|||||||
visibility: "public" | "private" | "limited";
|
visibility: "public" | "private" | "limited";
|
||||||
starredReposOrg: string;
|
starredReposOrg: string;
|
||||||
preserveOrgStructure: boolean;
|
preserveOrgStructure: boolean;
|
||||||
mirrorStrategy?: "preserve" | "single-org" | "flat-user";
|
mirrorStrategy?: "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
personalReposOrg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,6 +107,7 @@ export function mapDbToUiConfig(dbConfig: any): {
|
|||||||
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github",
|
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github",
|
||||||
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false,
|
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false,
|
||||||
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy,
|
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy,
|
||||||
|
personalReposOrg: dbConfig.giteaConfig?.personalReposOrg,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mirrorOptions: MirrorOptions = {
|
const mirrorOptions: MirrorOptions = {
|
||||||
|
|||||||
37
src/pages/404.astro
Normal file
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>
|
||||||
@@ -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,
|
||||||
@@ -174,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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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";
|
||||||
@@ -96,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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -93,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
|
||||||
@@ -108,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,
|
||||||
});
|
});
|
||||||
@@ -134,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"),
|
||||||
|
|||||||
@@ -112,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),
|
||||||
},
|
},
|
||||||
@@ -133,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 || "",
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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";
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
@@ -68,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,
|
||||||
@@ -84,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
|
||||||
@@ -100,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,
|
||||||
@@ -135,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 || "",
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
82
src/pages/api/organizations/[id].ts
Normal file
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
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const githubOptions = [
|
|||||||
{ name: 'Mirror Wiki', desc: 'Mirror wiki pages 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: 'Mirror Organizations', desc: 'Mirror organization repositories', default: 'false' },
|
||||||
{ name: 'Only Mirror Orgs', desc: 'Only mirror organization repositories', default: 'false' },
|
{ name: 'Only Mirror Orgs', desc: 'Only mirror organization repositories', default: 'false' },
|
||||||
{ name: 'Preserve Org Structure', desc: 'Maintain organization structure in Gitea', default: 'false' },
|
|
||||||
{ name: 'Skip Forks', desc: 'Exclude repositories that are forks', 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' }
|
{ name: 'Skip Starred Issues', desc: 'Skip issues for starred repositories', default: 'false' }
|
||||||
];
|
];
|
||||||
@@ -249,6 +248,74 @@ const giteaOptions = [
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<div class="pl-4 border-l-2 border-primary/20">
|
||||||
<h5 class="font-semibold mb-3">To create a Gitea access token:</h5>
|
<h5 class="font-semibold mb-3">To create a Gitea access token:</h5>
|
||||||
<ol class="space-y-2 text-sm">
|
<ol class="space-y-2 text-sm">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type Config as ConfigType } from "@/lib/db/schema";
|
import { type Config as ConfigType } from "@/lib/db/schema";
|
||||||
|
|
||||||
export type GiteaOrgVisibility = "public" | "private" | "limited";
|
export type GiteaOrgVisibility = "public" | "private" | "limited";
|
||||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user";
|
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
|
||||||
export interface GiteaConfig {
|
export interface GiteaConfig {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -12,6 +12,7 @@ export interface GiteaConfig {
|
|||||||
starredReposOrg: string;
|
starredReposOrg: string;
|
||||||
preserveOrgStructure: boolean;
|
preserveOrgStructure: boolean;
|
||||||
mirrorStrategy?: MirrorStrategy; // New field for the strategy
|
mirrorStrategy?: MirrorStrategy; // New field for the strategy
|
||||||
|
personalReposOrg?: string; // Override destination for personal repos
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleConfig {
|
export interface ScheduleConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user