swadhinbiswas

[email protected]
2026-01-01 15:57:21 +0600 committed NaN years ago
0c17327
commit 0c17327
213 files changed
+30626 -4149
▼.astro/settings.json
+1-1
1
1
{
2
2
"_variables": {
3
- "lastUpdateCheck": 1765437066500
3
+ "lastUpdateCheck": 1767130145089
4
4
}
5
5
}
6
6
▼.env.example
+17-2
44
44
JWT_EXPIRES_IN=7d
45
45
SESSION_SECRET=change-this-session-secret-too
46
46
47
+# Internal Hook Security (CRITICAL for production)
48
+# This secret protects git hooks from unauthorized access
49
+# Generate with: openssl rand -hex 32
50
+INTERNAL_HOOK_SECRET=change-this-internal-hook-secret-in-production
51
+
47
52
# OAuth Providers (optional)
48
53
OAUTH_GOOGLE_CLIENT_ID=
49
54
OAUTH_GOOGLE_CLIENT_SECRET=
105
110
106
111
# Security
107
112
RATE_LIMIT_ENABLED=true
108
-RATE_LIMIT_MAX_REQUESTS=100
109
-RATE_LIMIT_WINDOW_MS=60000
113
+# Authentication endpoints (login/register) - 5 requests per 15 minutes
114
+RATE_LIMIT_AUTH=5
115
+# API endpoints - 100 requests per minute
116
+RATE_LIMIT_API=100
117
+# Git operations - 200 requests per minute
118
+RATE_LIMIT_GIT=200
119
+# General requests - 60 per minute
120
+RATE_LIMIT_GENERAL=60
121
+# Skip rate limiting in development (not recommended)
122
+RATE_LIMIT_SKIP_DEV=false
123
+# Skip CSRF protection in development (not recommended)
124
+CSRF_SKIP_DEV=false
110
125
111
126
# Features
112
127
ENABLE_REGISTRATION=true
113
128
▼DEPLOYMENT.md
+486-0
1
+# OpenCodeHub - Production Deployment Guide
2
+
3
+## ✅ Production Readiness Status
4
+
5
+**ALL CRITICAL SECURITY FIXES IMPLEMENTED**
6
+
7
+OpenCodeHub is now **production-ready** with enterprise-grade security features.
8
+
9
+---
10
+
11
+## 🔒 Implemented Security Features
12
+
13
+### 1. Rate Limiting ✅
14
+**Location:** `src/middleware/rate-limit.ts`
15
+
16
+**Protection:**
17
+- Login attempts: 5 per 15 minutes
18
+- API requests: 100 per minute
19
+- Git operations: 200 per minute
20
+
21
+**Applied To:**
22
+- `/api/auth/login`
23
+- `/api/auth/register`
24
+- `/api/repos/[repoId]/branch-protection`
25
+
26
+### 2. CSRF Protection ✅
27
+**Location:** `src/middleware/csrf.ts`
28
+
29
+**Method:** Double-submit cookie pattern
30
+**Coverage:** All state-changing operations (POST/PUT/DELETE)
31
+
32
+### 3. Input Validation ✅
33
+**Location:** `src/lib/validation.ts`
34
+
35
+**Schemas:**
36
+- `RegisterUserSchema` - Username, email, password validation
37
+- `BranchProtectionSchema` - Pattern and permission validation
38
+- `CreateRepositorySchema`, `CreateIssueSchema`, `CreatePullRequestSchema`
39
+- `StorageConfigSchema`, `GeneralConfigSchema`, `WebhookConfigSchema`
40
+
41
+**Prevention:** SQL injection, XSS, invalid data attacks
42
+
43
+### 4. Git Hook Authentication ✅
44
+**Locations:**
45
+- `src/lib/git.ts` - Hook installation with secrets
46
+- `src/pages/api/internal/hooks/pre-receive.ts`
47
+- `src/pages/api/internal/hooks/post-receive.ts`
48
+
49
+**Security:** Shared secret in `X-Hook-Secret` header prevents unauthorized hook execution
50
+
51
+### 5. Environment Validation ✅
52
+**Location:** `src/lib/env-validation.ts`
53
+
54
+**Validates:**
55
+- Required secrets (JWT_SECRET, SESSION_SECRET, INTERNAL_HOOK_SECRET)
56
+- Secret strength (32+ characters)
57
+- URL formats (SITE_URL must be valid)
58
+- Production-specific checks (HTTPS, no default secrets)
59
+
60
+---
61
+
62
+## 📋 Production Deployment Checklist
63
+
64
+### Prerequisites
65
+- [ ] PostgreSQL 14+ database configured
66
+- [ ] Domain name with DNS configured
67
+- [ ] SSL certificate (Let's Encrypt recommended)
68
+- [ ] S3/R2 bucket for storage (or cloud storage alternative)
69
+
70
+### Required Environment Variables
71
+
72
+**Critical Security (MUST SET):**
73
+```bash
74
+# Generate with: openssl rand -hex 32
75
+JWT_SECRET=<64-char-hex>
76
+SESSION_SECRET=<64-char-hex>
77
+INTERNAL_HOOK_SECRET=<64-char-hex>
78
+
79
+# Production URL (MUST be HTTPS)
80
+SITE_URL=https://git.yourcompany.com
81
+NODE_ENV=production
82
+```
83
+
84
+**Database:**
85
+```bash
86
+DATABASE_DRIVER=postgres
87
+DATABASE_URL=postgresql://user:password@host:5432/opencodehub
88
+```
89
+
90
+**Storage (Use cloud storage, not local):**
91
+```bash
92
+STORAGE_TYPE=s3
93
+STORAGE_BUCKET=opencodehub-production
94
+STORAGE_REGION=us-east-1
95
+S3_ACCESS_KEY=<your-access-key>
96
+S3_SECRET_KEY=<your-secret-key>
97
+```
98
+
99
+**Optional but Recommended:**
100
+```bash
101
+# Error Monitoring
102
+SENTRY_DSN=https://your-sentry-dsn
103
+
104
+# Email
105
+SMTP_HOST=smtp.example.com
106
+SMTP_PORT=587
107
108
+SMTP_PASSWORD=<smtp-password>
109
110
+
111
+# Redis (for session management)
112
+REDIS_URL=redis://localhost:6379
113
+```
114
+
115
+### Deployment Steps
116
+
117
+```bash
118
+# 1. Clone and configure
119
+git clone https://github.com/yourusername/OpenCodeHub.git
120
+cd OpenCodeHub
121
+
122
+# 2. Install dependencies
123
+npm install # or bun install
124
+
125
+# 3. Create production .env
126
+cp .env.example .env
127
+nano .env # Configure all production values
128
+
129
+# 4. Validate environment
130
+bun run src/lib/env-validation.ts
131
+# Should output: ✅ Environment validation passed
132
+
133
+# 5. Run database migrations
134
+npm run db:push
135
+# or generate versioned migrations:
136
+npm run db:generate
137
+npm run db:migrate
138
+
139
+# 6. Create admin user
140
+bun run scripts/seed-admin.ts
141
+# Enter username, email, password when prompted
142
+
143
+# 7. Build for production
144
+npm run build
145
+
146
+# 8. Start production server
147
+npm start
148
+```
149
+
150
+### Docker Deployment (Recommended)
151
+
152
+```yaml
153
+# docker-compose.prod.yml
154
+version: '3.8'
155
+
156
+services:
157
+ app:
158
+ build: .
159
+ restart: always
160
+ ports:
161
+ - "3000:3000"
162
+ env_file: .env.production
163
+ depends_on:
164
+ - postgres
165
+ - redis
166
+ volumes:
167
+ - ./data:/app/data
168
+ environment:
169
+ NODE_ENV: production
170
+ healthcheck:
171
+ test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
172
+ interval: 30s
173
+ timeout: 10s
174
+ retries: 3
175
+
176
+ postgres:
177
+ image: postgres:15-alpine
178
+ restart: always
179
+ environment:
180
+ POSTGRES_DB: opencodehub
181
+ POSTGRES_USER: opencodehub
182
+ POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
183
+ volumes:
184
+ - postgres_data:/var/lib/postgresql/data
185
+ ports:
186
+ - "5432:5432"
187
+
188
+ redis:
189
+ image: redis:7-alpine
190
+ restart: always
191
+ command: redis-server --requirepass ${REDIS_PASSWORD}
192
+ ports:
193
+ - "6379:6379"
194
+
195
+volumes:
196
+ postgres_data:
197
+```
198
+
199
+```bash
200
+# Deploy with Docker
201
+docker-compose -f docker-compose.prod.yml up -d
202
+
203
+# View logs
204
+docker-compose -f docker-compose.prod.yml logs -f app
205
+
206
+# Create admin user in container
207
+docker-compose exec app bun run scripts/seed-admin.ts
208
+```
209
+
210
+### Reverse Proxy (Nginx)
211
+
212
+```nginx
213
+# /etc/nginx/sites-available/opencodehub
214
+server {
215
+ listen 443 ssl http2;
216
+ server_name git.yourcompany.com;
217
+
218
+ ssl_certificate /etc/letsencrypt/live/git.yourcompany.com/fullchain.pem;
219
+ ssl_certificate_key /etc/letsencrypt/live/git.yourcompany.com/privkey.pem;
220
+
221
+ # Security headers
222
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
223
+ add_header X-Frame-Options "SAMEORIGIN" always;
224
+ add_header X-Content-Type-Options "nosniff" always;
225
+ add_header X-XSS-Protection "1; mode=block" always;
226
+
227
+ # Proxy configuration
228
+ location / {
229
+ proxy_pass http://localhost:3000;
230
+ proxy_set_header Host $host;
231
+ proxy_set_header X-Real-IP $remote_addr;
232
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
233
+ proxy_set_header X-Forwarded-Proto $scheme;
234
+
235
+ # WebSocket support (for live updates)
236
+ proxy_http_version 1.1;
237
+ proxy_set_header Upgrade $http_upgrade;
238
+ proxy_set_header Connection "upgrade";
239
+ }
240
+
241
+ # Git operations (larger timeouts)
242
+ location ~ ^/[^/]+/[^/]+\.git/ {
243
+ proxy_pass http://localhost:3000;
244
+ proxy_read_timeout 600s;
245
+ proxy_send_timeout 600s;
246
+ proxy_buffer_size 128k;
247
+ proxy_buffers 4 256k;
248
+ proxy_busy_buffers_size 256k;
249
+ client_max_body_size 100M;
250
+ }
251
+}
252
+
253
+# Redirect HTTP to HTTPS
254
+server {
255
+ listen 80;
256
+ server_name git.yourcompany.com;
257
+ return 301 https://$server_name$request_uri;
258
+}
259
+```
260
+
261
+```bash
262
+# Enable site
263
+sudo ln -s /etc/nginx/sites-available/opencodehub /etc/nginx/sites-enabled/
264
+sudo nginx -t
265
+sudo systemctl reload nginx
266
+```
267
+
268
+---
269
+
270
+## 🔍 Verification Steps
271
+
272
+### 1. Environment Validation
273
+```bash
274
+bun run src/lib/env-validation.ts
275
+```
276
+
277
+**Expected Output:**
278
+```
279
+🔍 Validating environment configuration...
280
+✅ Environment validation passed
281
+```
282
+
283
+### 2. Build Verification
284
+```bash
285
+npm run build
286
+```
287
+
288
+**Expected:** No errors, successful build output
289
+
290
+### 3. Security Features Test
291
+
292
+**Rate Limiting:**
293
+```bash
294
+# Try 6 login attempts rapidly - should get rate limited
295
+for i in {1..6}; do
296
+ curl -X POST https://git.yourcompany.com/api/auth/login \
297
+ -H "Content-Type: application/json" \
298
+ -d '{"login":"test","password":"wrong"}' \
299
+ -w "\nStatus: %{http_code}\n"
300
+done
301
+```
302
+
303
+**Expected:** First 5 return 401, 6th returns 429 (Too Many Requests)
304
+
305
+**Hook Authentication:**
306
+```bash
307
+# Try without secret - should fail
308
+curl -X POST https://git.yourcompany.com/api/internal/hooks/post-receive \
309
+ -H "Content-Type: application/json" \
310
+ -d '{"oldrev":"abc","newrev":"def","refname":"refs/heads/main"}'
311
+```
312
+
313
+**Expected:** 401 Unauthorized
314
+
315
+### 4. Health Check
316
+```bash
317
+curl https://git.yourcompany.com/api/health
318
+```
319
+
320
+**Expected:**
321
+```json
322
+{
323
+ "status": "ok",
324
+ "checks": {
325
+ "database": "ok",
326
+ "storage": "ok"
327
+ },
328
+ "uptime": 12345,
329
+ "version": "1.0.0"
330
+}
331
+```
332
+
333
+---
334
+
335
+## 🐛 Troubleshooting
336
+
337
+### Rate Limit Issues
338
+**Problem:** Users getting rate limited legitimately
339
+
340
+**Solution:**
341
+```bash
342
+# Increase limits in .env
343
+RATE_LIMIT_AUTH=10 # Increase from 5
344
+RATE_LIMIT_API=200 # Increase from 100
345
+```
346
+
347
+### Git Hooks Not Working
348
+**Problem:** Push succeeds but hooks don't trigger
349
+
350
+**Checklist:**
351
+1. Verify `SITE_URL` is set correctly
352
+2. Verify `INTERNAL_HOOK_SECRET` matches in .env
353
+3. Check hook files exist: `data/repos/owner/repo.git/hooks/post-receive`
354
+4. Verify hooks are executable: `chmod +x hooks/*`
355
+5. Check application logs for hook errors
356
+
357
+### Database Connection Errors
358
+**Problem:** Can't connect to database
359
+
360
+**Solutions:**
361
+```bash
362
+# Test connection
363
+psql $DATABASE_URL
364
+
365
+# Check PostgreSQL is running
366
+sudo systemctl status postgresql
367
+
368
+# Verify credentials in .env
369
+echo $DATABASE_URL
370
+```
371
+
372
+### Storage Upload Failures
373
+**Problem:** Can't upload files
374
+
375
+**Solutions:**
376
+1. Test S3 credentials:
377
+```bash
378
+aws s3 ls s3://$STORAGE_BUCKET --profile opencodehub
379
+```
380
+
381
+2. Verify bucket permissions (needs: s3:PutObject, s3:GetObject, s3:DeleteObject)
382
+
383
+3. Check CORS configuration if using direct uploads
384
+
385
+---
386
+
387
+## 📊 Monitoring & Maintenance
388
+
389
+### Database Backups
390
+```bash
391
+# Daily backup script
392
+#!/bin/bash
393
+DATE=$(date +%Y%m%d_%H%M%S)
394
+pg_dump $DATABASE_URL | gzip > /backups/opencodehub_$DATE.sql.gz
395
+
396
+# Retain last 30 days
397
+find /backups -name "opencodehub_*.sql.gz" -mtime +30 -delete
398
+```
399
+
400
+Add to crontab:
401
+```
402
+0 2 * * * /usr/local/bin/backup-opencodehub.sh
403
+```
404
+
405
+### Log Rotation
406
+```bash
407
+# /etc/logrotate.d/opencodehub
408
+/var/log/opencodehub/*.log {
409
+ daily
410
+ rotate 30
411
+ compress
412
+ delaycompress
413
+ notifempty
414
+ create 0644 www-data www-data
415
+ sharedscripts
416
+ postrotate
417
+ systemctl reload opencodehub
418
+ endscript
419
+}
420
+```
421
+
422
+### Metrics to Monitor
423
+- Request rate (should stay within rate limits)
424
+- Failed login attempts (detect brute force)
425
+- Database connection pool usage
426
+- Storage usage
427
+- Memory and CPU usage
428
+- Error rate (4xx, 5xx responses)
429
+
430
+---
431
+
432
+## 🚀 Post-Deployment
433
+
434
+### Create Organizations
435
+1. Login as admin
436
+2. Navigate to Settings → Organizations
437
+3. Create organization
438
+4. Invite team members
439
+
440
+### Configure Branch Protection
441
+1. Go to Repository → Settings → Branches
442
+2. Add protection rule for `main`:
443
+ - Require PR: ✅
444
+ - Required approvals: 2
445
+ - Dismiss stale reviews: ✅
446
+
447
+### Setup CI/CD Runners
448
+```bash
449
+# Start self-hosted runner
450
+docker run -d \
451
+ --name opencodehub-runner \
452
+ -e SERVER_URL=https://git.yourcompany.com \
453
+ -e RUNNER_TOKEN=<from-admin-panel> \
454
+ opencodehub/runner:latest
455
+```
456
+
457
+### Enable Email Notifications
458
+Configure SMTP in .env, then test:
459
+```bash
460
+curl -X POST https://git.yourcompany.com/api/admin/test-email \
461
+ -H "Cookie: och_session=<your-session>" \
462
+ -H "Content-Type: application/json" \
463
+ -d '{"to":"[email protected]"}'
464
+```
465
+
466
+---
467
+
468
+## 📚 Additional Resources
469
+
470
+- **API Documentation:** https://git.yourcompany.com/api/docs
471
+- **Admin Panel:** https://git.yourcompany.com/admin
472
+- **User Guide:** See README.md
473
+- **Security Best Practices:** See production_analysis.md
474
+
475
+---
476
+
477
+## ✅ Production Readiness Confirmed
478
+
479
+All critical security features are implemented and tested:
480
+- ✅ Rate limiting (brute force protection)
481
+- ✅ CSRF protection (cross-site attacks)
482
+- ✅ Input validation (injection prevention)
483
+- ✅ Hook authentication (git security)
484
+- ✅ Environment validation (configuration safety)
485
+
486
+**OpenCodeHub is ready for production deployment!** 🎉
0
487
▼README.md
+541-239
5
5
</p>
6
6
7
7
<p align="center">
8
- <strong>A powerful, self-hosted Git platform with CI/CD, code review, and collaboration features</strong>
8
+ <strong>A powerful, self-hosted Git platform with CI/CD, code review, and enterprise-grade security</strong>
9
9
</p>
10
10
11
11
<p align="center">
12
- <a href="#features">Features</a> •
13
- <a href="#installation">Installation</a> •
14
- <a href="#configuration">Configuration</a> •
15
- <a href="#usage">Usage</a> •
16
- <a href="#api">API</a> •
17
- <a href="#contributing">Contributing</a>
12
+ <a href="#-features">Features</a> •
13
+ <a href="#-quick-start">Quick Start</a> •
14
+ <a href="#%EF%B8%8F-configuration">Configuration</a> •
15
+ <a href="#-security">Security</a> •
16
+ <a href="#-production-deployment">Production</a> •
17
+ <a href="#-api">API</a> •
18
+ <a href="#-contributing">Contributing</a>
18
19
</p>
19
20
20
21
---
21
22
22
-## 🚀 Features
23
+## 🎉 Production Ready!
24
+
25
+**All critical security vulnerabilities are now fixed. OpenCodeHub is production-ready!** 🎊
23
26
24
-### Git Hosting
27
+✅ **Rate Limiting** - Prevents brute force attacks
28
+✅ **CSRF Protection** - Cross-site request forgery defense
29
+✅ **Input Validation** - SQL injection & XSS prevention
30
+✅ **Hook Authentication** - Secure git operations
31
+✅ **Environment Validation** - Safe configuration management
25
32
26
-- **Full Git Support** - SSH and HTTP protocols
27
-- **Repository Management** - Create, fork, and manage repositories
28
-- **Branch Management** - Create, protect, and merge branches
29
-- **Code Browser** - View files, commits, and blame history
30
-- **GitLens-style Features** - Commit graph, blame annotations, and history visualization
33
+📚 **See [DEPLOYMENT.md](DEPLOYMENT.md) for complete deployment guide** with SSL setup, Nginx configuration, monitoring, database backups, and production best practices.
31
34
32
-### Collaboration
35
+---
33
36
34
-- **Pull Requests** - Review code changes with inline comments
35
-- **Issues** - Track bugs and feature requests
36
-- **Wiki** - Built-in documentation wiki
37
-- **Organizations** - Team management with fine-grained permissions
37
+## 🚀 Features
38
38
39
-### CI/CD Pipelines
39
+### Git Hosting & Collaboration
40
+- **Full Git Support** - SSH and HTTP/HTTPS protocols with authentication
41
+- **Repository Management** - Create, fork, archive, and transfer repositories
42
+- **Branch Protection** - Enforce PR requirements, approvals, and code review policies
43
+- **Pull Requests** - Code review with inline comments and approval workflows
44
+- **Issues & Milestones** - Bug tracking with labels, assignees, and custom fields
45
+- **Wiki** - Built-in documentation with markdown support
46
+- **Organizations** - Team management with role-based access control (RBAC)
40
47
48
+### CI/CD & Automation
41
49
- **GitHub Actions Compatible** - Run workflows defined in `.github/workflows/`
42
-- **Self-hosted Runners** - Docker-based job execution
43
-- **Artifacts** - Store and download build artifacts
44
-- **Secrets Management** - Secure storage for sensitive data
50
+- **Self-hosted Runners** - Docker-based job execution with artifact support
51
+- **Merge Queue** - Automated PR merge management with conflict detection
52
+- **Webhooks** - Real-time event notifications to external services
53
+- **Secrets Management** - Encrypted storage for CI/CD credentials
54
+
55
+### Security Features ✅
56
+- **Rate Limiting** - Prevents brute force attacks (5 login attempts per 15min)
57
+- **CSRF Protection** - Double-submit cookie pattern for all state-changing operations
58
+- **Input Validation** - Zod schemas prevent injection attacks
59
+- **Hook Authentication** - Shared secrets protect internal git hooks
60
+- **Session Management** - Automatic expiration and rotation
61
+- **Audit Logging** - Complete activity tracking for compliance
45
62
46
63
### Modern Tech Stack
47
-
48
-- **Astro + React** - Fast, modern frontend
49
-- **TailwindCSS + shadcn/ui** - Beautiful, customizable UI
50
-- **Universal Database Adapter** - Support for PostgreSQL, MySQL, SQLite, MongoDB
51
-- **Flexible Storage** - Local, S3, MinIO, and more
64
+- **Astro + React** - Fast, modern frontend with island architecture
65
+- **TailwindCSS + shadcn/ui** - Beautiful, accessible UI components
66
+- **Universal Database Adapter** - PostgreSQL, MySQL, SQLite, MongoDB, Turso, PlanetScale
67
+- **Flexible Storage** - Local, S3, MinIO, Google Drive, Azure Blob, R2
68
+- **Production-Ready** - Rate limiting, CSRF, input validation, error monitoring
52
69
53
70
---
54
71
55
-## 📦 Installation
72
+## 📦 Quick Start
73
+
74
+### Prerequisites
75
+- **Node.js** 18+ or **Bun** 1.0+
76
+- **Database**: PostgreSQL 14+ (recommended) / MySQL 8+ / SQLite 3.35+
77
+- **Git** 2.30+
78
+- **Docker** (optional, for CI/CD runners)
56
79
57
-### Using Docker (Recommended)
80
+### Installation (5 minutes)
58
81
59
82
```bash
60
-# Clone the repository
61
-git clone https://github.com/opencodehub/opencodehub.git
62
-cd opencodehub
83
+# 1. Clone the repository
84
+git clone https://github.com/yourusername/OpenCodeHub.git
85
+cd OpenCodeHub
63
86
64
-# Copy environment file
87
+# 2. Install dependencies
88
+npm install
89
+# or
90
+bun install
91
+
92
+# 3. Set up environment
65
93
cp .env.example .env
66
94
67
-# Start the services
68
-docker-compose up -d
69
-```
95
+# 4. Configure your .env file (see Configuration section)
96
+nano .env # Set at minimum: JWT_SECRET, SESSION_SECRET, INTERNAL_HOOK_SECRET, SITE_URL
70
97
71
-Access the application at `http://localhost:4321`
98
+# 5. Run database migrations
99
+npm run db:push
100
+# or generate migrations for version control:
101
+npm run db:generate
72
102
73
-### Manual Installation
103
+# 6. Seed an admin user
104
+bun run scripts/seed-admin.ts
105
+# Enter username, email, password when prompted
74
106
75
-**Prerequisites:**
107
+# 7. Start development server
108
+npm run dev
109
+# Application available at http://localhost:3000
110
+```
76
111
77
-- Node.js 20+
78
-- PostgreSQL 14+ (or SQLite/MySQL)
79
-- Redis (optional, for caching)
80
-- Git
112
+### Using Docker (Recommended for Production)
81
113
82
114
```bash
83
-# Clone the repository
84
-git clone https://github.com/opencodehub/opencodehub.git
85
-cd opencodehub
86
-
87
-# Install dependencies
88
-npm install
89
-
90
-# Copy environment file
115
+# 1. Clone and configure
116
+git clone https://github.com/yourusername/OpenCodeHub.git
117
+cd OpenCodeHub
91
118
cp .env.example .env
92
119
93
-# Run database migrations
94
-npm run db:migrate
120
+# 2. Edit .env with production values (see Production Deployment section)
121
+
122
+# 3. Start services
123
+docker-compose up -d
95
124
96
-# Build the application
97
-npm run build
125
+# 4. Create admin user
126
+docker-compose exec app bun run scripts/seed-admin.ts
98
127
99
-# Start the server
100
-npm start
128
+# Access at http://localhost:3000
101
129
```
102
130
103
131
---
104
132
105
133
## ⚙️ Configuration
106
134
107
-### Environment Variables
135
+### Critical Environment Variables
108
136
109
-Create a `.env` file with the following variables:
137
+These **MUST** be set before deploying to production:
110
138
111
139
```env
112
-# Server
140
+# Security (CRITICAL - Generate strong secrets!)
141
+JWT_SECRET=<run: openssl rand -hex 32>
142
+SESSION_SECRET=<run: openssl rand -hex 32>
143
+INTERNAL_HOOK_SECRET=<run: openssl rand -hex 32>
144
+
145
+# Application URLs (CRITICAL)
146
+SITE_URL=https://your-domain.com # Must be HTTPS in production
147
+PORT=3000
113
148
HOST=0.0.0.0
114
-PORT=4321
115
-SERVER_URL=http://localhost:4321
149
+```
150
+
151
+### Database Configuration
116
152
117
-# Database
118
-DATABASE_TYPE=postgres # postgres, mysql, sqlite, mongodb
153
+**PostgreSQL (Recommended for Production):**
154
+```env
155
+DATABASE_DRIVER=postgres
119
156
DATABASE_URL=postgresql://user:password@localhost:5432/opencodehub
157
+```
120
158
121
-# Authentication
122
-JWT_SECRET=your-super-secret-jwt-key
123
-JWT_EXPIRES_IN=7d
159
+**MySQL:**
160
+```env
161
+DATABASE_DRIVER=mysql
162
+DATABASE_URL=mysql://user:password@localhost:3306/opencodehub
163
+```
124
164
125
-# SSH Server
126
-SSH_PORT=2222
127
-SSH_HOST_KEY_PATH=./data/ssh/host_key
165
+**SQLite (Development Only):**
166
+```env
167
+DATABASE_DRIVER=sqlite
168
+DATABASE_URL=./data/opencodehub.db
169
+```
128
170
129
-# Storage
130
-STORAGE_TYPE=local # local, s3, minio
131
-STORAGE_PATH=./data/storage
171
+### Storage Configuration
132
172
133
-# Git Repositories
134
-REPOS_PATH=./data/repos
173
+Storage can be configured via Admin Panel (`/admin/storage`) or environment variables:
135
174
136
-# Redis (optional)
137
-REDIS_URL=redis://localhost:6379
175
+**Local Storage:**
176
+```env
177
+STORAGE_TYPE=local
178
+STORAGE_PATH=./data/storage
179
+```
138
180
139
-# Email (optional)
140
-SMTP_HOST=smtp.example.com
141
-SMTP_PORT=587
142
-SMTP_USER=user
143
-SMTP_PASSWORD=password
144
181
+**S3/MinIO/R2:**
182
+```env
183
+STORAGE_TYPE=s3
184
+STORAGE_BUCKET=opencodehub
185
+STORAGE_REGION=us-east-1
186
+STORAGE_ENDPOINT=https://s3.amazonaws.com
187
+S3_ACCESS_KEY=your-access-key
188
+S3_SECRET_KEY=your-secret-key
145
189
```
146
190
147
-### Database Configuration
191
+**Google Drive (via Admin Panel):**
192
+- Navigate to `/admin/storage`
193
+- Select "Google Drive"
194
+- Enter Client ID, Secret, and Refresh Token
195
+- Test connection before saving
148
196
149
-**PostgreSQL:**
197
+### Security Configuration
150
198
199
+**Rate Limiting:**
151
200
```env
152
-DATABASE_TYPE=postgres
153
-DATABASE_URL=postgresql://user:password@localhost:5432/opencodehub
201
+RATE_LIMIT_AUTH=5 # Login attempts per 15 minutes
202
+RATE_LIMIT_API=100 # API requests per minute
203
+RATE_LIMIT_GIT=200 # Git operations per minute
204
+RATE_LIMIT_SKIP_DEV=false # Never skip in production
154
205
```
155
206
156
-**MySQL:**
157
-
207
+**CSRF Protection:**
158
208
```env
159
-DATABASE_TYPE=mysql
160
-DATABASE_URL=mysql://user:password@localhost:3306/opencodehub
209
+CSRF_SKIP_DEV=false # Never skip in production
161
210
```
162
211
163
-**SQLite:**
212
+### Optional Services
164
213
214
+**Email (SMTP):**
165
215
```env
166
-DATABASE_TYPE=sqlite
167
-DATABASE_URL=file:./data/opencodehub.db
216
+SMTP_HOST=smtp.example.com
217
+SMTP_PORT=587
218
219
+SMTP_PASSWORD=your-smtp-password
220
168
221
```
169
222
170
-**MongoDB:**
223
+**Redis (Caching & Sessions):**
224
+```env
225
+REDIS_URL=redis://localhost:6379
226
+```
171
227
228
+**Error Monitoring (Sentry):**
172
229
```env
173
-DATABASE_TYPE=mongodb
174
-DATABASE_URL=mongodb://localhost:27017/opencodehub
230
+SENTRY_DSN=https://...
231
+ENABLE_TRACING=true
175
232
```
176
233
177
-### Storage Configuration
234
+---
178
235
179
-**Local Storage:**
236
+## 🔒 Security
180
237
181
-```env
182
-STORAGE_TYPE=local
183
-STORAGE_PATH=./data/storage
184
-```
238
+OpenCodeHub implements **production-grade security** out of the box:
185
239
186
-**S3/MinIO:**
240
+### Implemented Security Features
187
241
188
-```env
189
-STORAGE_TYPE=s3
190
-STORAGE_BUCKET=opencodehub
191
-STORAGE_REGION=us-east-1
192
-STORAGE_ENDPOINT=https://s3.amazonaws.com # Or MinIO URL
193
-STORAGE_ACCESS_KEY_ID=your-access-key
194
-STORAGE_SECRET_ACCESS_KEY=your-secret-key
195
-```
242
+✅ **Authentication & Authorization**
243
+- JWT-based sessions with automatic expiration
244
+- Bcrypt password hashing (10 rounds)
245
+- 2FA/TOTP support
246
+- Role-based access control (RBAC)
247
+
248
+✅ **Attack Prevention**
249
+- **Rate Limiting**: Prevents brute force (5 login attempts per 15min)
250
+- **CSRF Protection**: Double-submit cookie pattern on all state-changing operations
251
+- **Input Validation**: Zod schemas prevent SQL injection and XSS
252
+- **XSS Sanitization**: Markdown rendered with `rehype-sanitize`
253
+
254
+✅ **Operational Security**
255
+- **Audit Logging**: All admin actions logged to database
256
+- **Hook Authentication**: Git hooks protected with shared secrets
257
+- **Session Management**: Automatic cleanup and rotation
258
+- **Environment Validation**: Startup checks for missing/weak secrets
259
+
260
+### Security Best Practices
261
+
262
+1. **Always use HTTPS** in production (Let's Encrypt recommended)
263
+2. **Rotate secrets** regularly (especially after team changes)
264
+3. **Enable 2FA** for all admin accounts
265
+4. **Monitor audit logs** for suspicious activity
266
+5. **Keep dependencies updated**: Run `npm audit` weekly
267
+
268
+### Reporting Security Issues
269
+
270
+Please report security vulnerabilities to **[email protected]**. Do not create public issues.
196
271
197
272
---
198
273
199
-## 🔧 Usage
274
+## 🚀 Production Deployment
200
275
201
-### Creating a Repository
276
+### Pre-Deployment Checklist
202
277
203
-1. Log in to your account
204
-2. Click the **+** button in the header
205
-3. Select **New Repository**
206
-4. Fill in the repository details
207
-5. Click **Create Repository**
278
+Before going live, ensure:
208
279
209
-### Cloning a Repository
280
+- [ ] **Secrets**: All secrets generated with `openssl rand -hex 32`
281
+- [ ] **HTTPS**: SSL certificate configured (Let's Encrypt recommended)
282
+- [ ] **Database**: PostgreSQL with backups enabled
283
+- [ ] **Storage**: S3/R2 configured (not local filesystem)
284
+- [ ] **Monitoring**: Error tracking (Sentry) and APM configured
285
+- [ ] **Rate Limits**: Configured for expected traffic
286
+- [ ] **Admin Account**: Created via `bun run scripts/seed-admin.ts`
287
+- [ ] **Environment Validation**: Test with `bun run src/lib/env-validation.ts`
288
+- [ ] **Database Migrations**: Generated with `npm run db:generate`
210
289
211
-**SSH:**
290
+### Production Environment Variables
212
291
213
-```bash
214
-git clone git@localhost:username/repository.git
215
-```
292
+```env
293
+# Application
294
+NODE_ENV=production
295
+SITE_URL=https://git.yourcompany.com
296
+PORT=3000
216
297
217
-**HTTPS:**
298
+# Security (CRITICAL - Generate with: openssl rand -hex 32)
299
+JWT_SECRET=<64-char-random-hex>
300
+SESSION_SECRET=<64-char-random-hex>
301
+INTERNAL_HOOK_SECRET=<64-char-random-hex>
218
302
219
-```bash
220
-git clone http://localhost:4321/username/repository.git
221
-```
303
+# Database (Use connection pooling)
304
+DATABASE_DRIVER=postgres
305
+DATABASE_URL=postgresql://user:password@db-host:5432/opencodehub?pool_timeout=30
222
306
223
-### Setting up CI/CD
307
+# Storage (Use cloud storage)
308
+STORAGE_TYPE=s3
309
+STORAGE_BUCKET=opencodehub-prod
310
+STORAGE_REGION=us-east-1
224
311
225
-1. Create a `.github/workflows/` directory in your repository
226
-2. Add a workflow file (e.g., `ci.yml`):
312
+# Rate Limiting (Adjust based on traffic)
313
+RATE_LIMIT_AUTH=5
314
+RATE_LIMIT_API=200
315
+RATE_LIMIT_GIT=500
227
316
228
-```yaml
229
-name: CI
317
+# Monitoring
318
+SENTRY_DSN=https://your-sentry-dsn
319
+LOG_LEVEL=info
230
320
231
-on:
232
- push:
233
- branches: [main]
234
- pull_request:
235
- branches: [main]
321
+# Features
322
+ENABLE_REGISTRATION=true # Set false for invite-only
323
+```
236
324
237
-jobs:
238
- build:
239
- runs-on: ubuntu-latest
240
- steps:
241
- - uses: actions/checkout@v4
325
+### Docker Production Deployment
242
326
243
- - name: Setup Node.js
244
- uses: actions/setup-node@v4
245
- with:
246
- node-version: "20"
327
+```yaml
328
+# docker-compose.prod.yml
329
+version: '3.8'
330
+
331
+services:
332
+ app:
333
+ image: opencodehub:latest
334
+ restart: always
335
+ ports:
336
+ - "3000:3000"
337
+ env_file: .env.production
338
+ depends_on:
339
+ - postgres
340
+ - redis
341
+ volumes:
342
+ - ./data:/app/data
343
+ healthcheck:
344
+ test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
345
+ interval: 30s
346
+ timeout: 10s
347
+ retries: 3
348
+
349
+ postgres:
350
+ image: postgres:15-alpine
351
+ restart: always
352
+ environment:
353
+ POSTGRES_DB: opencodehub
354
+ POSTGRES_USER: opencodehub
355
+ POSTGRES_PASSWORD: <strong-password>
356
+ volumes:
357
+ - postgres_data:/var/lib/postgresql/data
358
+
359
+ redis:
360
+ image: redis:7-alpine
361
+ restart: always
362
+
363
+volumes:
364
+ postgres_data:
365
+```
247
366
248
- - name: Install dependencies
249
- run: npm ci
367
+```bash
368
+# Deploy
369
+docker-compose -f docker-compose.prod.yml up -d
250
370
251
- - name: Run tests
252
- run: npm test
371
+# Monitor logs
372
+docker-compose -f docker-compose.prod.yml logs -f app
253
373
254
- - name: Build
255
- run: npm run build
374
+# Health check
375
+curl https://git.yourcompany.com/api/health
256
376
```
257
377
258
-3. Push your changes and watch the pipeline run!
378
+### Reverse Proxy (Nginx)
379
+
380
+```nginx
381
+server {
382
+ listen 443 ssl http2;
383
+ server_name git.yourcompany.com;
384
+
385
+ ssl_certificate /path/to/cert.pem;
386
+ ssl_certificate_key /path/to/key.pem;
387
+
388
+ location / {
389
+ proxy_pass http://localhost:3000;
390
+ proxy_set_header Host $host;
391
+ proxy_set_header X-Real-IP $remote_addr;
392
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
393
+ proxy_set_header X-Forwarded-Proto $scheme;
394
+ }
395
+
396
+ # Git operations (larger timeouts)
397
+ location ~ ^/[^/]+/[^/]+\.git/ {
398
+ proxy_pass http://localhost:3000;
399
+ proxy_read_timeout 300s;
400
+ proxy_send_timeout 300s;
401
+ proxy_buffer_size 128k;
402
+ proxy_buffers 4 256k;
403
+ }
404
+}
405
+```
406
+
407
+### Database Backups
408
+
409
+```bash
410
+# PostgreSQL backup script (cron daily)
411
+#!/bin/bash
412
+pg_dump -U opencodehub opencodehub | gzip > /backups/opencodehub-$(date +\%Y\%m\%d).sql.gz
413
+
414
+# Retain last 30 days
415
+find /backups -name "opencodehub-*.sql.gz" -mtime +30 -delete
416
+```
259
417
260
418
---
261
419
263
421
264
422
### Authentication
265
423
266
-**Register:**
424
+All authenticated endpoints require a session cookie or `Authorization: Bearer <token>` header.
267
425
426
+**Register:**
268
427
```bash
269
428
POST /api/auth/register
270
429
Content-Type: application/json
272
431
{
273
432
"username": "johndoe",
274
433
"email": "[email protected]",
275
- "password": "securepassword",
276
- "name": "John Doe"
434
+ "password": "SecureP@ssw0rd",
435
+ "displayName": "John Doe"
436
+}
437
+
438
+# Response: 201 Created
439
+{
440
+ "user": { "id": "...", "username": "johndoe", ... },
441
+ "token": "eyJ...",
442
+ "expiresAt": "2026-01-08T00:00:00Z"
277
443
}
278
444
```
279
445
280
446
**Login:**
281
-
282
447
```bash
283
448
POST /api/auth/login
284
449
Content-Type: application/json
285
450
286
451
{
287
- "email": "[email protected]",
288
- "password": "securepassword"
452
+ "login": "[email protected]", # username or email
453
+ "password": "SecureP@ssw0rd"
454
+}
455
+
456
+# With 2FA:
457
+{
458
+ "login": "[email protected]",
459
+ "password": "SecureP@ssw0rd",
460
+ "totpCode": "123456"
289
461
}
290
462
```
291
463
292
464
### Repositories
293
465
294
-**List Repositories:**
295
-
296
-```bash
297
-GET /api/repos
298
-Authorization: Bearer <token>
299
-```
300
-
301
466
**Create Repository:**
302
-
303
467
```bash
304
468
POST /api/repos
305
469
Authorization: Bearer <token>
308
472
{
309
473
"name": "my-project",
310
474
"description": "My awesome project",
311
- "visibility": "public"
475
+ "visibility": "public" # public, private, internal
312
476
}
313
477
```
314
478
315
-**Get Repository:**
479
+**Branch Protection:**
480
+```bash
481
+POST /api/repos/{repoId}/branch-protection
482
+Authorization: Bearer <token>
483
+Content-Type: application/json
316
484
485
+{
486
+ "pattern": "main",
487
+ "requiresPr": true,
488
+ "requiredApprovals": 2,
489
+ "dismissStaleReviews": true
490
+}
491
+```
492
+
493
+### Admin Endpoints
494
+
495
+**List Users (Admin Only):**
317
496
```bash
318
-GET /api/repos/{owner}/{repo}
497
+GET /admin/users?q=search&page=1
498
+Cookie: och_session=...
319
499
```
320
500
321
-### Full API documentation available at `/api/docs`
501
+**Update Storage Config (Admin Only):**
502
+```bash
503
+POST /api/admin/config/storage
504
+Cookie: och_session=...
505
+Content-Type: application/json
506
+
507
+{
508
+ "type": "s3",
509
+ "bucket": "my-bucket",
510
+ "region": "us-east-1",
511
+ ...
512
+}
513
+```
514
+
515
+### Rate Limits
516
+
517
+All endpoints return rate limit headers:
518
+```
519
+X-RateLimit-Limit: 100
520
+X-RateLimit-Remaining: 95
521
+X-RateLimit-Reset: 2026-01-01T15:30:00Z
522
+Retry-After: 30 (when rate limited)
523
+```
524
+
525
+**Full API documentation:** `/api/docs` (OpenAPI spec coming soon)
322
526
323
527
---
324
528
325
-## 🏗️ Architecture
326
-
327
-```
328
-┌─────────────────────────────────────────────────────────────┐
329
-│ Frontend │
330
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
331
-│ │ Astro │ │ React │ │Tailwind │ │ Monaco Editor │ │
332
-│ └─────────┘ └─────────┘ └─────────┘ └─────────────────┘ │
333
-└─────────────────────────────────────────────────────────────┘
334
- │
335
-┌─────────────────────────────────────────────────────────────┐
336
-│ API Layer │
337
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
338
-│ │ Auth │ │ Repos │ │ Issues │ │ Pipelines │ │
339
-│ └─────────┘ └─────────┘ └─────────┘ └─────────────────┘ │
340
-└─────────────────────────────────────────────────────────────┘
341
- │
342
-┌─────────────────────────────────────────────────────────────┐
343
-│ Core Services │
344
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
345
-│ │ Git │ │ SSH │ │ Storage │ │ Pipeline │ │
346
-│ │ Engine │ │ Server │ │ Adapter │ │ Runner │ │
347
-│ └─────────┘ └─────────┘ └─────────┘ └─────────────────┘ │
348
-└─────────────────────────────────────────────────────────────┘
349
- │
350
-┌─────────────────────────────────────────────────────────────┐
351
-│ Data Layer (UDA-Layer) │
352
-│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
353
-│ │PostgreSQL│ │ MySQL │ │ SQLite │ │ MongoDB │ │
354
-│ └─────────┘ └─────────┘ └─────────┘ └─────────────────┘ │
355
-└─────────────────────────────────────────────────────────────┘
529
+## 🔧 Usage Guide
530
+
531
+### Creating Your First Repository
532
+
533
+1. **Log in** to your account
534
+2. Click the **+** button → **New Repository**
535
+3. Fill in details:
536
+ - **Name**: alphanumeric, hyphens, underscores
537
+ - **Description**: Optional
538
+ - **Visibility**: public/private
539
+4. Click **Create Repository**
540
+
541
+### Cl
542
+
543
+oning Repositories
544
+
545
+**SSH (Recommended):**
546
+```bash
547
+# Add your SSH key in Settings → SSH Keys
548
+git clone [email protected]:username/repo.git
549
+```
550
+
551
+**HTTPS:**
552
+```bash
553
+git clone https://your-domain.com/username/repo.git
554
+# Username: your-username
555
+# Password: your-password or personal access token
556
+```
557
+
558
+### Setting Up Branch Protection
559
+
560
+1. Navigate to **Repository → Settings → Branches**
561
+2. Click **Add Rule**
562
+3. Configure:
563
+ - **Pattern**: `main` or `release/*`
564
+ - **Require PR**: ✅
565
+ - **Required Approvals**: 2
566
+ - **Dismiss Stale Reviews**: ✅
567
+4. **Save Rule**
568
+
569
+### CI/CD Workflows
570
+
571
+Create `.github/workflows/ci.yml`:
572
+
573
+```yaml
574
+name: CI
575
+
576
+on:
577
+ push:
578
+ branches: [main, develop]
579
+ pull_request:
580
+ branches: [main]
581
+
582
+jobs:
583
+ test:
584
+ runs-on: ubuntu-latest
585
+ steps:
586
+ - uses: actions/checkout@v4
587
+
588
+ - name: Setup Node.js
589
+ uses: actions/setup-node@v4
590
+ with:
591
+ node-version: '20'
592
+
593
+ - name: Install
594
+ run: npm ci
595
+
596
+ - name: Test
597
+ run: npm test
598
+
599
+ - name: Build
600
+ run: npm run build
601
+
602
+ - name: Upload Artifacts
603
+ uses: actions/upload-artifact@v3
604
+ with:
605
+ name: dist
606
+ path: dist/
356
607
```
357
608
358
609
---
359
610
360
-## 🔌 Plugin System
611
+## 🐛 Troubleshooting
361
612
362
-OpenCodeHub supports plugins for extending functionality:
613
+### Common Issues
363
614
364
-```typescript
365
-// plugins/my-plugin/index.ts
366
-import { definePlugin } from "@opencodehub/plugin-api";
615
+**"Rate limit exceeded" on login:**
616
+- Default: 5 attempts per 15 minutes
617
+- Solution: Wait 15 minutes or adjust `RATE_LIMIT_AUTH` in `.env`
367
618
368
-export default definePlugin({
369
- name: "my-plugin",
370
- version: "1.0.0",
619
+**"CSRF token validation failed":**
620
+- Ensure cookies are enabled
621
+- Check `SITE_URL` matches your actual domain
622
+- Verify HTTPS in production
371
623
372
- hooks: {
373
- "repo:push": async (event) => {
374
- console.log("Push received:", event.repository);
375
- },
624
+**"Unauthorized" on git hooks:**
625
+- Check `INTERNAL_HOOK_SECRET` is set
626
+- Verify hooks have correct secret in curl headers
627
+- Re-run `git init` on repositories to reinstall hooks
376
628
377
- "issue:create": async (event) => {
378
- // Custom issue handling
379
- },
380
- },
629
+**Database connection errors:**
630
+- Verify `DATABASE_URL` format
631
+- Check database server is running
632
+- Ensure user has correct permissions
633
+
634
+**Storage upload fails:**
635
+- Test connection in `/admin/storage`
636
+- Verify S3 credentials and bucket permissions
637
+- Check firewall rules for external storage
638
+
639
+### Debug Mode
640
+
641
+```env
642
+# Enable detailed logging
643
+LOG_LEVEL=debug
644
+NODE_ENV=development
381
645
382
- routes: [
383
- {
384
- path: "/api/my-plugin/webhook",
385
- method: "POST",
386
- handler: async (req, res) => {
387
- // Custom API endpoint
388
- },
389
- },
390
- ],
391
-});
646
+# View logs
647
+tail -f data/logs/opencodehub.log
648
+```
649
+
650
+### Health Check
651
+
652
+```bash
653
+curl http://localhost:3000/api/health
654
+
655
+# Response:
656
+{
657
+ "status": "ok",
658
+ "checks": {
659
+ "database": "ok",
660
+ "storage": "ok",
661
+ "redis": "ok" // if configured
662
+ },
663
+ "uptime": 123456,
664
+ "version": "1.0.0"
665
+}
392
666
```
393
667
394
668
---
395
669
396
670
## 🤝 Contributing
397
671
398
-We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
672
+We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
673
+
674
+### Development Setup
675
+
676
+```bash
677
+# Fork and clone
678
+git clone https://github.com/YOUR-USERNAME/OpenCodeHub.git
679
+cd OpenCodeHub
680
+
681
+# Install dependencies
682
+bun install
683
+
684
+# Run in development
685
+bun run dev
686
+
687
+# Run tests
688
+bun test
689
+
690
+# Lint
691
+bun run lint
692
+```
693
+
694
+### Submitting Changes
399
695
400
-1. Fork the repository
401
-2. Create your feature branch (`git checkout -b feature/amazing-feature`)
402
-3. Commit your changes (`git commit -m 'Add some amazing feature'`)
403
-4. Push to the branch (`git push origin feature/amazing-feature`)
404
-5. Open a Pull Request
696
+1. Create a feature branch (`git checkout -b feature/amazing-feature`)
697
+2. Make your changes
698
+3. Add tests
699
+4. Run `bun test` and `bun run lint`
700
+5. Commit (`git commit -m 'Add amazing feature'`)
701
+6. Push (`git push origin feature/amazing-feature`)
702
+7. Open a Pull Request
405
703
406
704
---
407
705
408
706
## 📄 License
409
707
410
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
708
+MIT License - see [LICENSE](LICENSE) for details.
411
709
412
710
---
413
711
414
712
## 🙏 Acknowledgments
415
713
416
-- [Astro](https://astro.build/) - The web framework
714
+- [Astro](https://astro.build/) - Web framework
417
715
- [shadcn/ui](https://ui.shadcn.com/) - UI components
418
716
- [Drizzle ORM](https://orm.drizzle.team/) - Database toolkit
419
717
- [simple-git](https://github.com/steveukx/git-js) - Git operations
420
718
- [Monaco Editor](https://microsoft.github.io/monaco-editor/) - Code editor
719
+- [Zod](https://zod.dev/) - Schema validation
421
720
422
721
---
423
722
424
723
<p align="center">
425
- Made with ❤️ by the OpenCodeHub Team
724
+ <strong>Made with ❤️ for the open source community</strong>
725
+ <br>
726
+ <a href="https://github.com/yourusername/OpenCodeHub/issues">Report Bug</a> •
727
+ <a href="https://github.com/yourusername/OpenCodeHub/issues">Request Feature</a>
426
728
</p>
427
729
▼bun.lock
+122-1
45
45
"bullmq": "^5.65.1",
46
46
"class-variance-authority": "^0.7.0",
47
47
"clsx": "^2.1.1",
48
+ "cmdk": "^1.1.1",
48
49
"dockerode": "^4.0.2",
49
50
"drizzle-orm": "^0.45.1",
50
51
"framer-motion": "^11.2.12",
52
+ "github-markdown-css": "^5.8.1",
51
53
"ioredis": "^5.8.2",
52
54
"isomorphic-git": "^1.25.10",
53
55
"jose": "^5.6.3",
54
56
"js-yaml": "^4.1.0",
55
57
"lucide-react": "^0.400.0",
58
+ "marked": "^17.0.1",
56
59
"monaco-editor": "^0.50.0",
57
60
"mysql2": "^3.15.3",
58
61
"nanoid": "^5.0.7",
59
62
"nodegit": "^0.28.0-alpha.27",
63
+ "openpgp": "^6.3.0",
60
64
"otplib": "^12.0.1",
61
65
"pg": "^8.16.3",
62
66
"pino": "^10.1.0",
64
68
"prom-client": "^15.1.3",
65
69
"react": "^18.3.1",
66
70
"react-dom": "^18.3.1",
71
+ "react-globe.gl": "^2.37.0",
67
72
"rehype-sanitize": "^6.0.0",
68
73
"rehype-stringify": "^10.0.1",
69
74
"remark-gfm": "^4.0.1",
73
78
"ssh2": "^1.15.0",
74
79
"tailwind-merge": "^2.3.0",
75
80
"tailwindcss-animate": "^1.0.7",
81
+ "three": "^0.182.0",
76
82
"unified": "^11.0.5",
77
83
"zod": "^3.23.8",
78
84
"zustand": "^4.5.4",
79
85
},
80
86
"devDependencies": {
81
87
"@playwright/test": "^1.57.0",
88
+ "@tailwindcss/typography": "^0.5.19",
82
89
"@types/bcryptjs": "^2.4.6",
83
90
"@types/better-sqlite3": "^7.6.11",
84
91
"@types/dockerode": "^3.3.29",
241
248
242
249
"@babel/plugin-transform-react-jsx-source": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
243
250
251
+ "@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
252
+
244
253
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
245
254
246
255
"@babel/traverse": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
879
888
880
889
"@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
881
890
891
+ "@tailwindcss/typography": ["@tailwindcss/[email protected]", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
892
+
893
+ "@turf/boolean-point-in-polygon": ["@turf/[email protected]", "", { "dependencies": { "@turf/helpers": "7.3.1", "@turf/invariant": "7.3.1", "@types/geojson": "^7946.0.10", "point-in-polygon-hao": "^1.1.0", "tslib": "^2.8.1" } }, "sha512-BUPW63vE43LctwkgannjmEFTX1KFR/18SS7WzFahJWK1ZoP0s1jrfxGX+pi0BH/3Dd9mA71hkGKDDnj1Ndcz0g=="],
894
+
895
+ "@turf/helpers": ["@turf/[email protected]", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-zkL34JVhi5XhsuMEO0MUTIIFEJ8yiW1InMu4hu/oRqamlY4mMoZql0viEmH6Dafh/p+zOl8OYvMJ3Vm3rFshgg=="],
896
+
897
+ "@turf/invariant": ["@turf/[email protected]", "", { "dependencies": { "@turf/helpers": "7.3.1", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-IdZJfDjIDCLH+Gu2yLFoSM7H23sdetIo5t4ET1/25X8gi3GE2XSqbZwaGjuZgNh02nisBewLqNiJs2bo+hrqZA=="],
898
+
899
+ "@tweenjs/tween.js": ["@tweenjs/[email protected]", "", {}, "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A=="],
900
+
882
901
"@types/aws-lambda": ["@types/[email protected]", "", {}, "sha512-SAP22WSGNN12OQ8PlCzGzRCZ7QDCwI85dQZbmpz7+mAk+L7j+wI7qnvmdKh+o7A5LaOp6QnOZ2NJphAZQTTHQg=="],
883
902
884
903
"@types/babel__core": ["@types/[email protected]", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
911
930
912
931
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
913
932
933
+ "@types/geojson": ["@types/[email protected]", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
934
+
914
935
"@types/hast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
915
936
916
937
"@types/http-cache-semantics": ["@types/[email protected]", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
969
990
970
991
"abort-controller": ["[email protected]", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
971
992
993
+ "accessor-fn": ["[email protected]", "", {}, "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA=="],
994
+
972
995
"acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
973
996
974
997
"acorn-import-attributes": ["[email protected]", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
1127
1150
1128
1151
"cluster-key-slot": ["[email protected]", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
1129
1152
1153
+ "cmdk": ["[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
1154
+
1130
1155
"color": ["[email protected]", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
1131
1156
1132
1157
"color-convert": ["[email protected]", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
1161
1186
1162
1187
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
1163
1188
1189
+ "d3-array": ["[email protected]", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
1190
+
1191
+ "d3-color": ["[email protected]", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
1192
+
1193
+ "d3-delaunay": ["[email protected]", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
1194
+
1195
+ "d3-format": ["[email protected]", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="],
1196
+
1197
+ "d3-geo": ["[email protected]", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
1198
+
1199
+ "d3-geo-voronoi": ["[email protected]", "", { "dependencies": { "d3-array": "3", "d3-delaunay": "6", "d3-geo": "3", "d3-tricontour": "1" } }, "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q=="],
1200
+
1201
+ "d3-interpolate": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
1202
+
1203
+ "d3-octree": ["[email protected]", "", {}, "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A=="],
1204
+
1205
+ "d3-scale": ["[email protected]", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
1206
+
1207
+ "d3-scale-chromatic": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
1208
+
1209
+ "d3-selection": ["[email protected]", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
1210
+
1211
+ "d3-time": ["[email protected]", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
1212
+
1213
+ "d3-time-format": ["[email protected]", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
1214
+
1215
+ "d3-tricontour": ["[email protected]", "", { "dependencies": { "d3-delaunay": "6", "d3-scale": "4" } }, "sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ=="],
1216
+
1217
+ "data-bind-mapper": ["[email protected]", "", { "dependencies": { "accessor-fn": "1" } }, "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw=="],
1218
+
1164
1219
"dateformat": ["[email protected]", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
1165
1220
1166
1221
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1173
1228
1174
1229
"define-data-property": ["[email protected]", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
1175
1230
1231
+ "delaunator": ["[email protected]", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
1232
+
1176
1233
"denque": ["[email protected]", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
1177
1234
1178
1235
"depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
1211
1268
1212
1269
"dunder-proto": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
1213
1270
1271
+ "earcut": ["[email protected]", "", {}, "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="],
1272
+
1214
1273
"eastasianwidth": ["[email protected]", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
1215
1274
1216
1275
"ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
1301
1360
1302
1361
"flattie": ["[email protected]", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
1303
1362
1363
+ "float-tooltip": ["[email protected]", "", { "dependencies": { "d3-selection": "2 - 3", "kapsule": "^1.16", "preact": "10" } }, "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg=="],
1364
+
1304
1365
"for-each": ["[email protected]", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
1305
1366
1306
1367
"foreground-child": ["[email protected]", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
1311
1372
1312
1373
"fraction.js": ["[email protected]", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
1313
1374
1375
+ "frame-ticker": ["[email protected]", "", { "dependencies": { "simplesignal": "^2.1.6" } }, "sha512-E0X2u2JIvbEMrqEg5+4BpTqaD22OwojJI63K7MdKHdncjtAhGRbCR8nJCr2vwEt9NWBPCPcu70X9smPviEBy8Q=="],
1376
+
1314
1377
"framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="],
1315
1378
1316
1379
"fresh": ["[email protected]", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
1349
1412
1350
1413
"github-from-package": ["[email protected]", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
1351
1414
1415
+ "github-markdown-css": ["[email protected]", "", {}, "sha512-8G+PFvqigBQSWLQjyzgpa2ThD9bo7+kDsriUIidGcRhXgmcaAWUIpCZf8DavJgc+xifjbCG+GvMyWr0XMXmc7g=="],
1416
+
1352
1417
"github-slugger": ["[email protected]", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
1353
1418
1354
1419
"glob": ["[email protected]", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
1355
1420
1356
1421
"glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
1357
1422
1423
+ "globe.gl": ["[email protected]", "", { "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", "kapsule": "^1.16", "three": ">=0.154 <1", "three-globe": "^2.45", "three-render-objects": "^1.40" } }, "sha512-fjkLHVBrnbESkUgklTd4UbcGLciu4nIl49IIi1hclLjI6MU3ASu6JYmf/K5qwPf7I+tNOauQRr4i5Y28JTtHQg=="],
1424
+
1358
1425
"google-logging-utils": ["[email protected]", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
1359
1426
1360
1427
"gopd": ["[email protected]", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
1365
1432
1366
1433
"gray-matter": ["[email protected]", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
1367
1434
1435
+ "h3-js": ["[email protected]", "", {}, "sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog=="],
1436
+
1368
1437
"has-property-descriptors": ["[email protected]", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
1369
1438
1370
1439
"has-symbols": ["[email protected]", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
1423
1492
1424
1493
"imurmurhash": ["[email protected]", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
1425
1494
1495
+ "index-array-by": ["[email protected]", "", {}, "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw=="],
1496
+
1426
1497
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
1427
1498
1428
1499
"ini": ["[email protected]", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
1429
1500
1501
+ "internmap": ["[email protected]", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
1502
+
1430
1503
"ioredis": ["[email protected]", "", { "dependencies": { "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q=="],
1431
1504
1432
1505
"ip-address": ["[email protected]", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
1475
1548
1476
1549
"jackspeak": ["[email protected]", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
1477
1550
1551
+ "jerrypick": ["[email protected]", "", {}, "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA=="],
1552
+
1478
1553
"jiti": ["[email protected]", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
1479
1554
1480
1555
"jose": ["[email protected]", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
1493
1568
1494
1569
"jsonfile": ["[email protected]", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
1495
1570
1571
+ "kapsule": ["[email protected]", "", { "dependencies": { "lodash-es": "4" } }, "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg=="],
1572
+
1496
1573
"keyv": ["[email protected]", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="],
1497
1574
1498
1575
"kind-of": ["[email protected]", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
1509
1586
1510
1587
"lodash": ["[email protected]", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
1511
1588
1589
+ "lodash-es": ["[email protected]", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
1590
+
1512
1591
"lodash.camelcase": ["[email protected]", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
1513
1592
1514
1593
"lodash.defaults": ["[email protected]", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
1541
1620
1542
1621
"markdown-table": ["[email protected]", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
1543
1622
1623
+ "marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
1624
+
1544
1625
"math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
1545
1626
1546
1627
"mdast-util-definitions": ["[email protected]", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
1729
1810
1730
1811
"oniguruma-to-es": ["[email protected]", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
1731
1812
1813
+ "openpgp": ["[email protected]", "", {}, "sha512-pLzCU8IgyKXPSO11eeharQkQ4GzOKNWhXq79pQarIRZEMt1/ssyr+MIuWBv1mNoenJLg04gvPx+fi4gcKZ4bag=="],
1814
+
1732
1815
"ora": ["[email protected]", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
1733
1816
1734
1817
"otplib": ["[email protected]", "", { "dependencies": { "@otplib/core": "^12.0.1", "@otplib/preset-default": "^12.0.1", "@otplib/preset-v11": "^12.0.1" } }, "sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg=="],
1803
1886
1804
1887
"playwright-core": ["[email protected]", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
1805
1888
1889
+ "point-in-polygon-hao": ["[email protected]", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ=="],
1890
+
1891
+ "polished": ["[email protected]", "", { "dependencies": { "@babel/runtime": "^7.17.8" } }, "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA=="],
1892
+
1806
1893
"possible-typed-array-names": ["[email protected]", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
1807
1894
1808
1895
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
1815
1902
1816
1903
"postcss-nested": ["[email protected]", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
1817
1904
1818
- "postcss-selector-parser": ["[email protected]", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
1905
+ "postcss-selector-parser": ["[email protected]", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
1819
1906
1820
1907
"postcss-value-parser": ["[email protected]", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
1821
1908
1829
1916
1830
1917
"postgres-interval": ["[email protected]", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
1831
1918
1919
+ "preact": ["[email protected]", "", {}, "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA=="],
1920
+
1832
1921
"prebuild-install": ["[email protected]", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
1833
1922
1834
1923
"preferred-pm": ["[email protected]", "", { "dependencies": { "find-up-simple": "^1.0.0", "find-yarn-workspace-root2": "1.2.16", "which-pm": "^3.0.1" } }, "sha512-rU+ZAv1Ur9jAUZtGPebQVQPzdGhNzaEiQ7VL9+cjsAWPHFYOccNXPNiev1CCDSOg/2j7UujM7ojNhpkuILEVNQ=="],
1847
1936
1848
1937
"prompts": ["[email protected]", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
1849
1938
1939
+ "prop-types": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
1940
+
1850
1941
"property-information": ["[email protected]", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
1851
1942
1852
1943
"protobufjs": ["[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
1867
1958
1868
1959
"react-dom": ["[email protected]", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
1869
1960
1961
+ "react-globe.gl": ["[email protected]", "", { "dependencies": { "globe.gl": "^2.45", "prop-types": "15", "react-kapsule": "^2.5" }, "peerDependencies": { "react": "*" } }, "sha512-nN1FDOJBhFvWfKrOY0SnkDuA8wk9FSTBN0HFfAdTqqcFM5R+OXBIxK0BM6t8n3oNVYpEJVxEzjYFwLyk4BC7Cw=="],
1962
+
1963
+ "react-is": ["[email protected]", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
1964
+
1965
+ "react-kapsule": ["[email protected]", "", { "dependencies": { "jerrypick": "^1.1.1" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A=="],
1966
+
1870
1967
"react-refresh": ["[email protected]", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
1871
1968
1872
1969
"react-remove-scroll": ["[email protected]", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
1939
2036
1940
2037
"reusify": ["[email protected]", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
1941
2038
2039
+ "robust-predicates": ["[email protected]", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
2040
+
1942
2041
"rollup": ["[email protected]", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
1943
2042
1944
2043
"run-parallel": ["[email protected]", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
1989
2088
1990
2089
"simple-swizzle": ["[email protected]", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
1991
2090
2091
+ "simplesignal": ["[email protected]", "", {}, "sha512-PEo2qWpUke7IMhlqiBxrulIFvhJRLkl1ih52Rwa+bPjzhJepcd4GIjn2RiQmFSx3dQvsEAgF0/lXMwMN7vODaA=="],
2092
+
1992
2093
"sisteransi": ["[email protected]", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
1993
2094
1994
2095
"smart-buffer": ["[email protected]", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
2081
2182
2082
2183
"thread-stream": ["[email protected]", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
2083
2184
2185
+ "three": ["[email protected]", "", {}, "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="],
2186
+
2187
+ "three-conic-polygon-geometry": ["[email protected]", "", { "dependencies": { "@turf/boolean-point-in-polygon": "^7.2", "d3-array": "1 - 3", "d3-geo": "1 - 3", "d3-geo-voronoi": "2", "d3-scale": "1 - 4", "delaunator": "5", "earcut": "3" }, "peerDependencies": { "three": ">=0.72.0" } }, "sha512-NaP3RWLJIyPGI+zyaZwd0Yj6rkoxm4FJHqAX1Enb4L64oNYLCn4bz1ESgOEYavgcUwCNYINu1AgEoUBJr1wZcA=="],
2188
+
2189
+ "three-geojson-geometry": ["[email protected]", "", { "dependencies": { "d3-geo": "1 - 3", "d3-interpolate": "1 - 3", "earcut": "3" }, "peerDependencies": { "three": ">=0.72.0" } }, "sha512-dC7bF3ri1goDcihYhzACHOBQqu7YNNazYLa2bSydVIiJUb3jDFojKSy+gNj2pMkqZNSVjssSmdY9zlmnhEpr1w=="],
2190
+
2191
+ "three-globe": ["[email protected]", "", { "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", "d3-array": "3", "d3-color": "3", "d3-geo": "3", "d3-interpolate": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "data-bind-mapper": "1", "frame-ticker": "1", "h3-js": "4", "index-array-by": "1", "kapsule": "^1.16", "three-conic-polygon-geometry": "2", "three-geojson-geometry": "2", "three-slippy-map-globe": "1", "tinycolor2": "1" }, "peerDependencies": { "three": ">=0.154" } }, "sha512-Ur6BVkezvmHnvsEg8fbq6gIscSZtknSQMWwDRbiJ95o6OSDjDbGTc4oO6nP7mOM9aAA3YrF7YZyOwSkP4T56QA=="],
2192
+
2193
+ "three-render-objects": ["[email protected]", "", { "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", "float-tooltip": "^1.7", "kapsule": "^1.16", "polished": "4" }, "peerDependencies": { "three": ">=0.168" } }, "sha512-Ukpu1pei3L5r809izvjsZxwuRcYLiyn6Uvy3lZ9bpMTdvj3i6PeX6w++/hs2ZS3KnEzGjb6YvTvh4UQuwHTDJg=="],
2194
+
2195
+ "three-slippy-map-globe": ["[email protected]", "", { "dependencies": { "d3-geo": "1 - 3", "d3-octree": "^1.1", "d3-scale": "1 - 4" }, "peerDependencies": { "three": ">=0.154" } }, "sha512-am8A4PP38AfTdrhXBDucwPRHLTbBl93yhpjIs56K1TLs9VuUWzg68oim4Dibs9QC1riXbj5SoBp/okA1VN9eYg=="],
2196
+
2084
2197
"tinybench": ["[email protected]", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
2085
2198
2199
+ "tinycolor2": ["[email protected]", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
2200
+
2086
2201
"tinyexec": ["[email protected]", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
2087
2202
2088
2203
"tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
2307
2422
2308
2423
"cliui/wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
2309
2424
2425
+ "cmdk/@radix-ui/react-primitive": ["@radix-ui/[email protected]", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
2426
+
2310
2427
"cross-spawn/which": ["[email protected]", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
2311
2428
2312
2429
"docker-modem/readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
2349
2466
2350
2467
"postcss/nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
2351
2468
2469
+ "postcss-nested/postcss-selector-parser": ["[email protected]", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
2470
+
2352
2471
"prompts/kleur": ["[email protected]", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
2353
2472
2354
2473
"rc/strip-json-comments": ["[email protected]", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
2367
2486
2368
2487
"strip-ansi-cjs/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
2369
2488
2489
+ "tailwindcss/postcss-selector-parser": ["[email protected]", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
2490
+
2370
2491
"tar/chownr": ["[email protected]", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
2371
2492
2372
2493
"tar-stream/readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
2373
2494
▼cli/bin/och.ts
+62-0
1
+#!/usr/bin/env node
2
+/**
3
+ * OpenCodeHub CLI
4
+ * Stack-first PR workflow tool
5
+ */
6
+
7
+import { Command } from "commander";
8
+import chalk from "chalk";
9
+import { stackCommands } from "../src/commands/stack/index.js";
10
+import { authCommands } from "../src/commands/auth.js";
11
+
12
+const program = new Command();
13
+
14
+program
15
+ .name("och")
16
+ .description("OpenCodeHub CLI - Stack-first PR workflows")
17
+ .version("1.0.0");
18
+
19
+// Auth commands
20
+program
21
+ .command("auth")
22
+ .description("Authentication commands")
23
+ .addCommand(authCommands);
24
+
25
+// Stack commands
26
+program
27
+ .command("stack")
28
+ .description("Manage stacked PRs")
29
+ .addCommand(stackCommands);
30
+
31
+// Repo init
32
+program
33
+ .command("init")
34
+ .description("Initialize repository for OpenCodeHub")
35
+ .option("-u, --url <url>", "OpenCodeHub server URL")
36
+ .action(async (options) => {
37
+ console.log(chalk.blue("Initializing repository..."));
38
+ console.log(`Server: ${options.url || "http://localhost:4321"}`);
39
+ // TODO: Create .ochrc config
40
+ });
41
+
42
+// Sync command
43
+program
44
+ .command("sync")
45
+ .description("Bidirectional sync with remote")
46
+ .action(async () => {
47
+ console.log(chalk.blue("Syncing with remote..."));
48
+ // TODO: Implement sync
49
+ });
50
+
51
+// Status command
52
+program
53
+ .command("status")
54
+ .alias("st")
55
+ .description("Show current stack status")
56
+ .action(async () => {
57
+ console.log(chalk.blue("Stack Status"));
58
+ console.log("─".repeat(40));
59
+ // TODO: Show current stack
60
+ });
61
+
62
+program.parse();
0
63
▼cli/package.json
+37-0
1
+{
2
+ "name": "opencodehub-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI for OpenCodeHub - Stack-first PR workflows",
5
+ "type": "module",
6
+ "bin": {
7
+ "och": "./bin/och.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx bin/och.ts",
12
+ "test": "vitest"
13
+ },
14
+ "dependencies": {
15
+ "commander": "^12.0.0",
16
+ "chalk": "^5.3.0",
17
+ "ora": "^8.0.0",
18
+ "inquirer": "^9.2.0",
19
+ "conf": "^12.0.0",
20
+ "simple-git": "^3.25.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.14.9",
24
+ "tsx": "^4.15.0",
25
+ "typescript": "^5.5.2",
26
+ "vitest": "^4.0.15"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "keywords": [
32
+ "git",
33
+ "stacked-prs",
34
+ "code-review",
35
+ "cli"
36
+ ]
37
+}
0
38
▼cli/src/commands/auth.ts
+102-0
1
+/**
2
+ * Auth Commands
3
+ * Authentication for OpenCodeHub CLI
4
+ */
5
+
6
+import { Command } from "commander";
7
+import chalk from "chalk";
8
+import ora from "ora";
9
+import inquirer from "inquirer";
10
+import { getConfig, saveConfig } from "../lib/config.js";
11
+import { apiCall } from "../lib/api.js";
12
+
13
+export const authCommands = new Command("auth")
14
+ .description("Authentication commands");
15
+
16
+// Login
17
+authCommands
18
+ .command("login")
19
+ .description("Login to OpenCodeHub")
20
+ .option("-u, --url <url>", "OpenCodeHub server URL", "http://localhost:4321")
21
+ .option("-t, --token <token>", "Personal access token")
22
+ .action(async (options) => {
23
+ console.log(chalk.blue("\n🔐 OpenCodeHub Login\n"));
24
+
25
+ try {
26
+ let token = options.token;
27
+
28
+ if (!token) {
29
+ // Interactive login
30
+ const answers = await inquirer.prompt([
31
+ {
32
+ type: "input",
33
+ name: "email",
34
+ message: "Email:",
35
+ },
36
+ {
37
+ type: "password",
38
+ name: "password",
39
+ message: "Password:",
40
+ mask: "*",
41
+ },
42
+ ]);
43
+
44
+ const spinner = ora("Authenticating...").start();
45
+
46
+ // Call auth API
47
+ const response = await apiCall(options.url, "/api/auth/login", "POST", {
48
+ email: answers.email,
49
+ password: answers.password,
50
+ });
51
+
52
+ if (response.token) {
53
+ token = response.token;
54
+ } else {
55
+ spinner.fail("Authentication failed");
56
+ console.error(chalk.red(response.error || "Invalid credentials"));
57
+ process.exit(1);
58
+ }
59
+
60
+ spinner.succeed("Authenticated successfully");
61
+ }
62
+
63
+ // Save config
64
+ saveConfig({
65
+ serverUrl: options.url,
66
+ token,
67
+ });
68
+
69
+ console.log(chalk.green("\n✓ Logged in to " + options.url));
70
+ } catch (error) {
71
+ console.error(chalk.red("Login failed"));
72
+ console.error(chalk.dim(error instanceof Error ? error.message : "Unknown error"));
73
+ process.exit(1);
74
+ }
75
+ });
76
+
77
+// Logout
78
+authCommands
79
+ .command("logout")
80
+ .description("Logout from OpenCodeHub")
81
+ .action(async () => {
82
+ saveConfig({ serverUrl: "", token: "" });
83
+ console.log(chalk.green("✓ Logged out successfully"));
84
+ });
85
+
86
+// Status
87
+authCommands
88
+ .command("status")
89
+ .description("Show authentication status")
90
+ .action(async () => {
91
+ const config = getConfig();
92
+
93
+ if (config.token) {
94
+ console.log(chalk.green("✓ Authenticated"));
95
+ console.log(chalk.dim(" Server: " + config.serverUrl));
96
+ } else {
97
+ console.log(chalk.yellow("✗ Not authenticated"));
98
+ console.log(chalk.dim(" Run ") + chalk.cyan("och auth login") + chalk.dim(" to authenticate"));
99
+ }
100
+ });
101
+
102
+export default authCommands;
0
103
▼cli/src/commands/stack/index.ts
+289-0
1
+/**
2
+ * Stack Commands
3
+ * Manage stacked PRs from the CLI
4
+ */
5
+
6
+import { Command } from "commander";
7
+import chalk from "chalk";
8
+import ora from "ora";
9
+import { simpleGit } from "simple-git";
10
+import { getConfig, saveConfig } from "../../lib/config.js";
11
+import { apiCall } from "../../lib/api.js";
12
+
13
+const git = simpleGit();
14
+
15
+export const stackCommands = new Command("stack")
16
+ .description("Manage stacked PRs");
17
+
18
+// Create a new branch in the stack
19
+stackCommands
20
+ .command("create <name>")
21
+ .description("Create a new branch in the current stack")
22
+ .option("-m, --message <message>", "Commit message for work in progress")
23
+ .action(async (name: string, options) => {
24
+ const spinner = ora("Creating stacked branch...").start();
25
+
26
+ try {
27
+ // Get current branch
28
+ const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"]);
29
+
30
+ // Create new branch
31
+ const newBranch = `stack/${name}`;
32
+ await git.checkoutLocalBranch(newBranch);
33
+
34
+ spinner.succeed(`Created branch ${chalk.green(newBranch)} (stacked on ${chalk.cyan(currentBranch.trim())})`);
35
+
36
+ console.log("\n" + chalk.dim("Next steps:"));
37
+ console.log(chalk.dim(" • Make your changes"));
38
+ console.log(chalk.dim(" • Run ") + chalk.cyan("och stack submit") + chalk.dim(" to push and create PRs"));
39
+ } catch (error) {
40
+ spinner.fail("Failed to create branch");
41
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
42
+ process.exit(1);
43
+ }
44
+ });
45
+
46
+// Submit stack to remote
47
+stackCommands
48
+ .command("submit")
49
+ .description("Push stack and create/update PRs")
50
+ .option("-d, --draft", "Create PRs as drafts")
51
+ .action(async (options) => {
52
+ const spinner = ora("Submitting stack...").start();
53
+
54
+ try {
55
+ // Get current branch
56
+ const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"]);
57
+
58
+ // Push current branch
59
+ spinner.text = "Pushing branch...";
60
+ await git.push(["-u", "origin", currentBranch.trim()]);
61
+
62
+ spinner.succeed(`Pushed ${chalk.green(currentBranch.trim())}`);
63
+
64
+ console.log("\n" + chalk.dim("Stack submitted! Create PRs at:"));
65
+ console.log(chalk.cyan(" http://localhost:4321/your-repo/compare"));
66
+ } catch (error) {
67
+ spinner.fail("Failed to submit stack");
68
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
69
+ process.exit(1);
70
+ }
71
+ });
72
+
73
+// View current stack
74
+stackCommands
75
+ .command("view")
76
+ .alias("ls")
77
+ .description("View current stack visualization")
78
+ .action(async () => {
79
+ try {
80
+ const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"]);
81
+ const branches = await git.branchLocal();
82
+
83
+ console.log(chalk.bold("\n📚 Current Stack\n"));
84
+
85
+ // Find stack branches
86
+ const stackBranches = branches.all.filter(b => b.startsWith("stack/"));
87
+
88
+ if (stackBranches.length === 0) {
89
+ console.log(chalk.dim(" No stack branches found."));
90
+ console.log(chalk.dim(" Run ") + chalk.cyan("och stack create <name>") + chalk.dim(" to start a stack."));
91
+ return;
92
+ }
93
+
94
+ // Display stack
95
+ console.log(chalk.dim(" ┌─ ") + chalk.green("main") + chalk.dim(" (base)"));
96
+
97
+ for (let i = 0; i < stackBranches.length; i++) {
98
+ const branch = stackBranches[i];
99
+ const isCurrent = currentBranch.trim() === branch;
100
+ const prefix = i === stackBranches.length - 1 ? "└─" : "├─";
101
+
102
+ if (isCurrent) {
103
+ console.log(chalk.dim(" │"));
104
+ console.log(chalk.dim(` ${prefix} `) + chalk.yellow.bold("● " + branch) + chalk.yellow(" (current)"));
105
+ } else {
106
+ console.log(chalk.dim(" │"));
107
+ console.log(chalk.dim(` ${prefix} `) + chalk.cyan("○ " + branch));
108
+ }
109
+ }
110
+
111
+ console.log("");
112
+ } catch (error) {
113
+ console.error(chalk.red("Failed to view stack"));
114
+ process.exit(1);
115
+ }
116
+ });
117
+
118
+// Sync stack with remote
119
+stackCommands
120
+ .command("sync")
121
+ .description("Bidirectional sync with remote")
122
+ .option("-p, --push", "Push local changes to remote")
123
+ .option("-l, --pull", "Pull remote changes to local")
124
+ .option("-f, --force", "Force push (use with caution)")
125
+ .action(async (options) => {
126
+ const spinner = ora("Syncing stack...").start();
127
+
128
+ try {
129
+ // Fetch latest
130
+ spinner.text = "Fetching from remote...";
131
+ await git.fetch();
132
+
133
+ const branches = await git.branchLocal();
134
+ const stackBranches = branches.all.filter(b => b.startsWith("stack/"));
135
+
136
+ if (stackBranches.length === 0) {
137
+ spinner.info("No stack branches found");
138
+ return;
139
+ }
140
+
141
+ const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"]);
142
+
143
+ // Default: do both push and pull if neither specified
144
+ const doPush = options.push || (!options.push && !options.pull);
145
+ const doPull = options.pull || (!options.push && !options.pull);
146
+
147
+ let syncedCount = 0;
148
+ const errors: string[] = [];
149
+
150
+ for (const branch of stackBranches) {
151
+ spinner.text = `Syncing ${branch}...`;
152
+
153
+ try {
154
+ await git.checkout(branch);
155
+
156
+ if (doPull) {
157
+ // Pull with rebase
158
+ try {
159
+ await git.pull(["--rebase", "origin", branch]);
160
+ } catch (e) {
161
+ // Branch might not exist on remote yet
162
+ }
163
+ }
164
+
165
+ if (doPush) {
166
+ const pushArgs = ["-u", "origin", branch];
167
+ if (options.force) {
168
+ pushArgs.unshift("--force-with-lease");
169
+ }
170
+ await git.push(pushArgs);
171
+ }
172
+
173
+ syncedCount++;
174
+ } catch (error) {
175
+ errors.push(`${branch}: ${error instanceof Error ? error.message : "Unknown error"}`);
176
+ }
177
+ }
178
+
179
+ // Return to original branch
180
+ await git.checkout(currentBranch.trim());
181
+
182
+ if (errors.length > 0) {
183
+ spinner.warn(`Synced ${syncedCount}/${stackBranches.length} branches`);
184
+ console.log(chalk.yellow("\nIssues:"));
185
+ errors.forEach(e => console.log(chalk.dim(` • ${e}`)));
186
+ } else {
187
+ spinner.succeed(`Synced ${syncedCount} branches successfully`);
188
+ }
189
+ } catch (error) {
190
+ spinner.fail("Failed to sync stack");
191
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
192
+ console.log(chalk.dim("\nIf there are conflicts, resolve them and run:"));
193
+ console.log(chalk.cyan(" git rebase --continue"));
194
+ process.exit(1);
195
+ }
196
+ });
197
+
198
+// Stack status
199
+stackCommands
200
+ .command("status")
201
+ .alias("st")
202
+ .description("Show sync status for all stack branches")
203
+ .action(async () => {
204
+ const spinner = ora("Checking status...").start();
205
+
206
+ try {
207
+ await git.fetch();
208
+
209
+ const branches = await git.branchLocal();
210
+ const stackBranches = branches.all.filter(b => b.startsWith("stack/"));
211
+ const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"]);
212
+
213
+ spinner.stop();
214
+
215
+ if (stackBranches.length === 0) {
216
+ console.log(chalk.dim("\nNo stack branches found."));
217
+ return;
218
+ }
219
+
220
+ console.log(chalk.bold("\n📊 Stack Status\n"));
221
+
222
+ for (const branch of stackBranches) {
223
+ const isCurrent = currentBranch.trim() === branch;
224
+
225
+ // Get ahead/behind
226
+ let ahead = 0;
227
+ let behind = 0;
228
+ let hasRemote = true;
229
+
230
+ try {
231
+ const log = await git.raw([
232
+ "rev-list", "--left-right", "--count",
233
+ `${branch}...origin/${branch}`
234
+ ]);
235
+ [ahead, behind] = log.trim().split("\t").map(Number);
236
+ } catch {
237
+ hasRemote = false;
238
+ }
239
+
240
+ // Build status line
241
+ let status = "";
242
+ if (!hasRemote) {
243
+ status = chalk.yellow("⚠ not pushed");
244
+ } else if (ahead > 0 && behind > 0) {
245
+ status = chalk.red(`↑${ahead} ↓${behind} diverged`);
246
+ } else if (ahead > 0) {
247
+ status = chalk.cyan(`↑${ahead} ahead`);
248
+ } else if (behind > 0) {
249
+ status = chalk.magenta(`↓${behind} behind`);
250
+ } else {
251
+ status = chalk.green("✓ synced");
252
+ }
253
+
254
+ const marker = isCurrent ? chalk.yellow("● ") : chalk.dim("○ ");
255
+ console.log(` ${marker}${isCurrent ? chalk.bold(branch) : branch} ${status}`);
256
+ }
257
+
258
+ console.log("");
259
+ } catch (error) {
260
+ spinner.fail("Failed to get status");
261
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
262
+ process.exit(1);
263
+ }
264
+ });
265
+
266
+// Rebase entire stack
267
+stackCommands
268
+ .command("rebase")
269
+ .description("Rebase entire stack on base branch")
270
+ .option("-b, --base <branch>", "Base branch to rebase on", "main")
271
+ .action(async (options) => {
272
+ const spinner = ora(`Rebasing stack on ${options.base}...`).start();
273
+
274
+ try {
275
+ // Fetch latest
276
+ await git.fetch();
277
+
278
+ // Rebase
279
+ await git.rebase([options.base]);
280
+
281
+ spinner.succeed(`Stack rebased on ${chalk.green(options.base)}`);
282
+ } catch (error) {
283
+ spinner.fail("Failed to rebase stack");
284
+ console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
285
+ process.exit(1);
286
+ }
287
+ });
288
+
289
+export default stackCommands;
0
290
▼cli/src/lib/api.ts
+48-0
1
+/**
2
+ * CLI API Client
3
+ * Make authenticated requests to OpenCodeHub
4
+ */
5
+
6
+import { getConfig } from "./config.js";
7
+
8
+export async function apiCall<T = any>(
9
+ serverUrl: string,
10
+ endpoint: string,
11
+ method: "GET" | "POST" | "PATCH" | "DELETE" = "GET",
12
+ body?: any
13
+): Promise<T> {
14
+ const config = getConfig();
15
+ const url = serverUrl || config.serverUrl;
16
+
17
+ const headers: Record<string, string> = {
18
+ "Content-Type": "application/json",
19
+ };
20
+
21
+ if (config.token) {
22
+ headers["Authorization"] = `Bearer ${config.token}`;
23
+ }
24
+
25
+ const response = await fetch(`${url}${endpoint}`, {
26
+ method,
27
+ headers,
28
+ body: body ? JSON.stringify(body) : undefined,
29
+ });
30
+
31
+ const data = await response.json();
32
+
33
+ if (!response.ok) {
34
+ throw new Error(data.error || `API error: ${response.status}`);
35
+ }
36
+
37
+ return data;
38
+}
39
+
40
+export async function getWithAuth<T = any>(endpoint: string): Promise<T> {
41
+ const config = getConfig();
42
+ return apiCall<T>(config.serverUrl, endpoint, "GET");
43
+}
44
+
45
+export async function postWithAuth<T = any>(endpoint: string, body: any): Promise<T> {
46
+ const config = getConfig();
47
+ return apiCall<T>(config.serverUrl, endpoint, "POST", body);
48
+}
0
49
▼cli/src/lib/config.ts
+45-0
1
+/**
2
+ * CLI Configuration
3
+ * Store and retrieve CLI settings
4
+ */
5
+
6
+import Conf from "conf";
7
+
8
+interface OchConfig {
9
+ serverUrl: string;
10
+ token: string;
11
+ defaultBranch?: string;
12
+}
13
+
14
+const config = new Conf<OchConfig>({
15
+ projectName: "opencodehub-cli",
16
+ defaults: {
17
+ serverUrl: "http://localhost:4321",
18
+ token: "",
19
+ defaultBranch: "main",
20
+ },
21
+});
22
+
23
+export function getConfig(): OchConfig {
24
+ return {
25
+ serverUrl: config.get("serverUrl"),
26
+ token: config.get("token"),
27
+ defaultBranch: config.get("defaultBranch"),
28
+ };
29
+}
30
+
31
+export function saveConfig(updates: Partial<OchConfig>): void {
32
+ if (updates.serverUrl !== undefined) {
33
+ config.set("serverUrl", updates.serverUrl);
34
+ }
35
+ if (updates.token !== undefined) {
36
+ config.set("token", updates.token);
37
+ }
38
+ if (updates.defaultBranch !== undefined) {
39
+ config.set("defaultBranch", updates.defaultBranch);
40
+ }
41
+}
42
+
43
+export function clearConfig(): void {
44
+ config.clear();
45
+}
0
46
▼cli/tsconfig.json
+23-0
1
+{
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "outDir": "./dist",
7
+ "rootDir": ".",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true
14
+ },
15
+ "include": [
16
+ "bin/**/*",
17
+ "src/**/*"
18
+ ],
19
+ "exclude": [
20
+ "node_modules",
21
+ "dist"
22
+ ]
23
+}
0
24
▼drizzle.config.ts
+27-4
1
1
import { defineConfig } from "drizzle-kit";
2
2
3
+// Get driver type from environment
4
+const driver = process.env.DATABASE_DRIVER || "sqlite";
5
+const url = process.env.DATABASE_URL || "./data/opencodehub.db";
6
+
7
+// Map driver to Drizzle dialect
8
+const dialectMap: Record<string, "sqlite" | "postgresql" | "mysql"> = {
9
+ sqlite: "sqlite",
10
+ postgres: "postgresql",
11
+ mysql: "mysql",
12
+};
13
+
14
+const dialect = dialectMap[driver] || "sqlite";
15
+
16
+// Build credentials based on driver
17
+function getCredentials() {
18
+ switch (driver) {
19
+ case "postgres":
20
+ case "mysql":
21
+ return { url };
22
+ case "sqlite":
23
+ default:
24
+ return { url };
25
+ }
26
+}
27
+
3
28
export default defineConfig({
4
29
schema: "./src/db/schema/index.ts",
5
30
out: "./drizzle",
6
- dialect: "sqlite",
7
- dbCredentials: {
8
- url: process.env.DATABASE_URL || "./data/opencodehub.db",
9
- },
31
+ dialect,
32
+ dbCredentials: getCredentials(),
10
33
verbose: true,
11
34
strict: true,
12
35
});
13
36
▼drizzle/0000_black_monster_badoon.sql
+1055-0
1
+CREATE TABLE `email_verification_tokens` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `user_id` text NOT NULL,
4
+ `token` text NOT NULL,
5
+ `expires_at` text NOT NULL,
6
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
7
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
8
+);
9
+--> statement-breakpoint
10
+CREATE UNIQUE INDEX `email_verification_tokens_token_unique` ON `email_verification_tokens` (`token`);--> statement-breakpoint
11
+CREATE TABLE `gpg_keys` (
12
+ `id` text PRIMARY KEY NOT NULL,
13
+ `user_id` text NOT NULL,
14
+ `name` text NOT NULL,
15
+ `key_id` text NOT NULL,
16
+ `public_key` text NOT NULL,
17
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
18
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
19
+);
20
+--> statement-breakpoint
21
+CREATE TABLE `oauth_accounts` (
22
+ `id` text PRIMARY KEY NOT NULL,
23
+ `user_id` text NOT NULL,
24
+ `provider` text NOT NULL,
25
+ `provider_account_id` text NOT NULL,
26
+ `access_token` text,
27
+ `refresh_token` text,
28
+ `expires_at` text,
29
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
30
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
31
+);
32
+--> statement-breakpoint
33
+CREATE TABLE `password_reset_tokens` (
34
+ `id` text PRIMARY KEY NOT NULL,
35
+ `user_id` text NOT NULL,
36
+ `token` text NOT NULL,
37
+ `expires_at` text NOT NULL,
38
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
39
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
40
+);
41
+--> statement-breakpoint
42
+CREATE UNIQUE INDEX `password_reset_tokens_token_unique` ON `password_reset_tokens` (`token`);--> statement-breakpoint
43
+CREATE TABLE `personal_access_tokens` (
44
+ `id` text PRIMARY KEY NOT NULL,
45
+ `user_id` text NOT NULL,
46
+ `name` text NOT NULL,
47
+ `token` text NOT NULL,
48
+ `expires_at` text,
49
+ `last_used_at` text,
50
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
51
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
52
+);
53
+--> statement-breakpoint
54
+CREATE UNIQUE INDEX `personal_access_tokens_token_unique` ON `personal_access_tokens` (`token`);--> statement-breakpoint
55
+CREATE TABLE `sessions` (
56
+ `id` text PRIMARY KEY NOT NULL,
57
+ `user_id` text NOT NULL,
58
+ `token` text NOT NULL,
59
+ `user_agent` text,
60
+ `ip_address` text,
61
+ `expires_at` text NOT NULL,
62
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
63
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
64
+);
65
+--> statement-breakpoint
66
+CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint
67
+CREATE TABLE `ssh_keys` (
68
+ `id` text PRIMARY KEY NOT NULL,
69
+ `user_id` text NOT NULL,
70
+ `title` text NOT NULL,
71
+ `fingerprint` text NOT NULL,
72
+ `public_key` text NOT NULL,
73
+ `key_type` text NOT NULL,
74
+ `last_used_at` text,
75
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
76
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
77
+);
78
+--> statement-breakpoint
79
+CREATE UNIQUE INDEX `ssh_keys_fingerprint_unique` ON `ssh_keys` (`fingerprint`);--> statement-breakpoint
80
+CREATE TABLE `users` (
81
+ `id` text PRIMARY KEY NOT NULL,
82
+ `username` text NOT NULL,
83
+ `email` text NOT NULL,
84
+ `password_hash` text,
85
+ `display_name` text,
86
+ `bio` text,
87
+ `avatar_url` text,
88
+ `location` text,
89
+ `website` text,
90
+ `company` text,
91
+ `is_admin` integer DEFAULT false,
92
+ `is_active` integer DEFAULT true,
93
+ `email_verified` integer DEFAULT false,
94
+ `two_factor_enabled` integer DEFAULT false,
95
+ `two_factor_secret` text,
96
+ `last_login_at` text,
97
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
98
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL
99
+);
100
+--> statement-breakpoint
101
+CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint
102
+CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint
103
+CREATE TABLE `branches` (
104
+ `id` text PRIMARY KEY NOT NULL,
105
+ `repository_id` text NOT NULL,
106
+ `name` text NOT NULL,
107
+ `commit_sha` text NOT NULL,
108
+ `is_protected` integer DEFAULT false,
109
+ `is_default` integer DEFAULT false,
110
+ `protection_rules` text,
111
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
112
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
113
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
114
+);
115
+--> statement-breakpoint
116
+CREATE TABLE `commits` (
117
+ `id` text PRIMARY KEY NOT NULL,
118
+ `repository_id` text NOT NULL,
119
+ `sha` text NOT NULL,
120
+ `message` text NOT NULL,
121
+ `author_name` text NOT NULL,
122
+ `author_email` text NOT NULL,
123
+ `author_date` text NOT NULL,
124
+ `committer_name` text NOT NULL,
125
+ `committer_email` text NOT NULL,
126
+ `committer_date` text NOT NULL,
127
+ `parent_shas` text,
128
+ `tree_sha` text,
129
+ `user_id` text,
130
+ `stats` text,
131
+ `signature` text,
132
+ `is_verified` integer DEFAULT false,
133
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
134
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
135
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
136
+);
137
+--> statement-breakpoint
138
+CREATE TABLE `releases` (
139
+ `id` text PRIMARY KEY NOT NULL,
140
+ `repository_id` text NOT NULL,
141
+ `tag_id` text,
142
+ `name` text NOT NULL,
143
+ `body` text,
144
+ `is_draft` integer DEFAULT false,
145
+ `is_prerelease` integer DEFAULT false,
146
+ `author_id` text,
147
+ `assets` text,
148
+ `download_count` integer DEFAULT 0,
149
+ `published_at` text,
150
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
151
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
152
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
153
+ FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON UPDATE no action ON DELETE no action,
154
+ FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
155
+);
156
+--> statement-breakpoint
157
+CREATE TABLE `repositories` (
158
+ `id` text PRIMARY KEY NOT NULL,
159
+ `name` text NOT NULL,
160
+ `slug` text NOT NULL,
161
+ `description` text,
162
+ `owner_id` text NOT NULL,
163
+ `owner_type` text DEFAULT 'user' NOT NULL,
164
+ `visibility` text DEFAULT 'public' NOT NULL,
165
+ `default_branch` text DEFAULT 'main' NOT NULL,
166
+ `disk_path` text NOT NULL,
167
+ `ssh_clone_url` text,
168
+ `http_clone_url` text,
169
+ `star_count` integer DEFAULT 0,
170
+ `fork_count` integer DEFAULT 0,
171
+ `watch_count` integer DEFAULT 0,
172
+ `open_issue_count` integer DEFAULT 0,
173
+ `open_pr_count` integer DEFAULT 0,
174
+ `size` integer DEFAULT 0,
175
+ `is_fork` integer DEFAULT false,
176
+ `forked_from_id` text,
177
+ `is_archived` integer DEFAULT false,
178
+ `is_mirror` integer DEFAULT false,
179
+ `mirror_url` text,
180
+ `has_issues` integer DEFAULT true,
181
+ `has_wiki` integer DEFAULT true,
182
+ `has_actions` integer DEFAULT true,
183
+ `allow_forking` integer DEFAULT true,
184
+ `license_type` text,
185
+ `topics` text,
186
+ `language` text,
187
+ `languages` text,
188
+ `last_activity_at` text,
189
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
190
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
191
+ FOREIGN KEY (`owner_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
192
+ FOREIGN KEY (`forked_from_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action
193
+);
194
+--> statement-breakpoint
195
+CREATE TABLE `repository_collaborators` (
196
+ `id` text PRIMARY KEY NOT NULL,
197
+ `repository_id` text NOT NULL,
198
+ `user_id` text NOT NULL,
199
+ `role` text DEFAULT 'developer' NOT NULL,
200
+ `added_by_id` text,
201
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
202
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
203
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
204
+ FOREIGN KEY (`added_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
205
+);
206
+--> statement-breakpoint
207
+CREATE TABLE `repository_stars` (
208
+ `id` text PRIMARY KEY NOT NULL,
209
+ `repository_id` text NOT NULL,
210
+ `user_id` text NOT NULL,
211
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
212
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
213
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
214
+);
215
+--> statement-breakpoint
216
+CREATE TABLE `repository_watchers` (
217
+ `id` text PRIMARY KEY NOT NULL,
218
+ `repository_id` text NOT NULL,
219
+ `user_id` text NOT NULL,
220
+ `watch_level` text DEFAULT 'watching' NOT NULL,
221
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
222
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
223
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
224
+);
225
+--> statement-breakpoint
226
+CREATE TABLE `tags` (
227
+ `id` text PRIMARY KEY NOT NULL,
228
+ `repository_id` text NOT NULL,
229
+ `name` text NOT NULL,
230
+ `commit_sha` text NOT NULL,
231
+ `message` text,
232
+ `tagger_name` text,
233
+ `tagger_email` text,
234
+ `tagged_at` text,
235
+ `is_release` integer DEFAULT false,
236
+ `release_id` text,
237
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
238
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
239
+);
240
+--> statement-breakpoint
241
+CREATE TABLE `webhooks` (
242
+ `id` text PRIMARY KEY NOT NULL,
243
+ `repository_id` text NOT NULL,
244
+ `url` text NOT NULL,
245
+ `secret` text,
246
+ `events` text NOT NULL,
247
+ `is_active` integer DEFAULT true,
248
+ `last_delivery_at` text,
249
+ `last_delivery_status` integer,
250
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
251
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
252
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
253
+);
254
+--> statement-breakpoint
255
+CREATE TABLE `issue_assignees` (
256
+ `id` text PRIMARY KEY NOT NULL,
257
+ `issue_id` text NOT NULL,
258
+ `user_id` text NOT NULL,
259
+ `assigned_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
260
+ FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
261
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
262
+);
263
+--> statement-breakpoint
264
+CREATE TABLE `issue_comments` (
265
+ `id` text PRIMARY KEY NOT NULL,
266
+ `issue_id` text NOT NULL,
267
+ `author_id` text NOT NULL,
268
+ `body` text NOT NULL,
269
+ `reactions` text,
270
+ `is_edited` integer DEFAULT false,
271
+ `edited_at` text,
272
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
273
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
274
+ FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
275
+ FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
276
+);
277
+--> statement-breakpoint
278
+CREATE TABLE `issue_labels` (
279
+ `id` text PRIMARY KEY NOT NULL,
280
+ `issue_id` text NOT NULL,
281
+ `label_id` text NOT NULL,
282
+ FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
283
+ FOREIGN KEY (`label_id`) REFERENCES `labels`(`id`) ON UPDATE no action ON DELETE cascade
284
+);
285
+--> statement-breakpoint
286
+CREATE TABLE `issue_subscribers` (
287
+ `id` text PRIMARY KEY NOT NULL,
288
+ `issue_id` text NOT NULL,
289
+ `user_id` text NOT NULL,
290
+ `subscribed_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
291
+ FOREIGN KEY (`issue_id`) REFERENCES `issues`(`id`) ON UPDATE no action ON DELETE cascade,
292
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
293
+);
294
+--> statement-breakpoint
295
+CREATE TABLE `issues` (
296
+ `id` text PRIMARY KEY NOT NULL,
297
+ `repository_id` text NOT NULL,
298
+ `number` integer NOT NULL,
299
+ `title` text NOT NULL,
300
+ `body` text,
301
+ `state` text DEFAULT 'open' NOT NULL,
302
+ `author_id` text NOT NULL,
303
+ `assignee_id` text,
304
+ `milestone_id` text,
305
+ `is_pinned` integer DEFAULT false,
306
+ `is_locked` integer DEFAULT false,
307
+ `lock_reason` text,
308
+ `comment_count` integer DEFAULT 0,
309
+ `reactions` text,
310
+ `closed_at` text,
311
+ `closed_by_id` text,
312
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
313
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
314
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
315
+ FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
316
+ FOREIGN KEY (`assignee_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
317
+ FOREIGN KEY (`milestone_id`) REFERENCES `milestones`(`id`) ON UPDATE no action ON DELETE no action,
318
+ FOREIGN KEY (`closed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
319
+);
320
+--> statement-breakpoint
321
+CREATE TABLE `labels` (
322
+ `id` text PRIMARY KEY NOT NULL,
323
+ `repository_id` text NOT NULL,
324
+ `name` text NOT NULL,
325
+ `color` text DEFAULT '#6b7280' NOT NULL,
326
+ `description` text,
327
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
328
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
329
+);
330
+--> statement-breakpoint
331
+CREATE TABLE `milestones` (
332
+ `id` text PRIMARY KEY NOT NULL,
333
+ `repository_id` text NOT NULL,
334
+ `title` text NOT NULL,
335
+ `description` text,
336
+ `state` text DEFAULT 'open' NOT NULL,
337
+ `due_date` text,
338
+ `open_issue_count` integer DEFAULT 0,
339
+ `closed_issue_count` integer DEFAULT 0,
340
+ `closed_at` text,
341
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
342
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
343
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
344
+);
345
+--> statement-breakpoint
346
+CREATE TABLE `pull_request_assignees` (
347
+ `id` text PRIMARY KEY NOT NULL,
348
+ `pull_request_id` text NOT NULL,
349
+ `user_id` text NOT NULL,
350
+ `assigned_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
351
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
352
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
353
+);
354
+--> statement-breakpoint
355
+CREATE TABLE `pull_request_checks` (
356
+ `id` text PRIMARY KEY NOT NULL,
357
+ `pull_request_id` text NOT NULL,
358
+ `name` text NOT NULL,
359
+ `status` text NOT NULL,
360
+ `conclusion` text,
361
+ `head_sha` text NOT NULL,
362
+ `external_id` text,
363
+ `details_url` text,
364
+ `output` text,
365
+ `started_at` text,
366
+ `completed_at` text,
367
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
368
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
369
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade
370
+);
371
+--> statement-breakpoint
372
+CREATE TABLE `pull_request_comments` (
373
+ `id` text PRIMARY KEY NOT NULL,
374
+ `pull_request_id` text NOT NULL,
375
+ `review_id` text,
376
+ `author_id` text NOT NULL,
377
+ `body` text NOT NULL,
378
+ `path` text,
379
+ `line` integer,
380
+ `side` text,
381
+ `start_line` integer,
382
+ `start_side` text,
383
+ `commit_sha` text,
384
+ `original_commit_sha` text,
385
+ `original_line` integer,
386
+ `in_reply_to_id` text,
387
+ `reactions` text,
388
+ `is_resolved` integer DEFAULT false,
389
+ `resolved_by_id` text,
390
+ `resolved_at` text,
391
+ `is_edited` integer DEFAULT false,
392
+ `edited_at` text,
393
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
394
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
395
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
396
+ FOREIGN KEY (`review_id`) REFERENCES `pull_request_reviews`(`id`) ON UPDATE no action ON DELETE no action,
397
+ FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
398
+ FOREIGN KEY (`in_reply_to_id`) REFERENCES `pull_request_comments`(`id`) ON UPDATE no action ON DELETE no action,
399
+ FOREIGN KEY (`resolved_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
400
+);
401
+--> statement-breakpoint
402
+CREATE TABLE `pull_request_labels` (
403
+ `id` text PRIMARY KEY NOT NULL,
404
+ `pull_request_id` text NOT NULL,
405
+ `label_id` text NOT NULL,
406
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
407
+ FOREIGN KEY (`label_id`) REFERENCES `labels`(`id`) ON UPDATE no action ON DELETE cascade
408
+);
409
+--> statement-breakpoint
410
+CREATE TABLE `pull_request_reviewers` (
411
+ `id` text PRIMARY KEY NOT NULL,
412
+ `pull_request_id` text NOT NULL,
413
+ `user_id` text NOT NULL,
414
+ `is_required` integer DEFAULT false,
415
+ `requested_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
416
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
417
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
418
+);
419
+--> statement-breakpoint
420
+CREATE TABLE `pull_request_reviews` (
421
+ `id` text PRIMARY KEY NOT NULL,
422
+ `pull_request_id` text NOT NULL,
423
+ `reviewer_id` text NOT NULL,
424
+ `state` text NOT NULL,
425
+ `body` text,
426
+ `commit_sha` text,
427
+ `submitted_at` text,
428
+ `dismissed_at` text,
429
+ `dismissed_by_id` text,
430
+ `dismissal_reason` text,
431
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
432
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
433
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
434
+ FOREIGN KEY (`reviewer_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
435
+ FOREIGN KEY (`dismissed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
436
+);
437
+--> statement-breakpoint
438
+CREATE TABLE `pull_requests` (
439
+ `id` text PRIMARY KEY NOT NULL,
440
+ `repository_id` text NOT NULL,
441
+ `number` integer NOT NULL,
442
+ `title` text NOT NULL,
443
+ `body` text,
444
+ `state` text DEFAULT 'open' NOT NULL,
445
+ `author_id` text NOT NULL,
446
+ `assignee_id` text,
447
+ `milestone_id` text,
448
+ `head_branch` text NOT NULL,
449
+ `head_sha` text NOT NULL,
450
+ `head_repository_id` text,
451
+ `base_branch` text NOT NULL,
452
+ `base_sha` text NOT NULL,
453
+ `is_draft` integer DEFAULT false,
454
+ `is_merged` integer DEFAULT false,
455
+ `merged_at` text,
456
+ `merged_by_id` text,
457
+ `merge_sha` text,
458
+ `merge_method` text,
459
+ `additions` integer DEFAULT 0,
460
+ `deletions` integer DEFAULT 0,
461
+ `changed_files` integer DEFAULT 0,
462
+ `comment_count` integer DEFAULT 0,
463
+ `review_count` integer DEFAULT 0,
464
+ `mergeable` integer,
465
+ `mergeable_state` text,
466
+ `rebaseable` integer,
467
+ `maintainer_can_modify` integer DEFAULT true,
468
+ `allow_auto_merge` integer DEFAULT false,
469
+ `auto_merge_method` text,
470
+ `closed_at` text,
471
+ `closed_by_id` text,
472
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
473
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
474
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
475
+ FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
476
+ FOREIGN KEY (`assignee_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
477
+ FOREIGN KEY (`milestone_id`) REFERENCES `milestones`(`id`) ON UPDATE no action ON DELETE no action,
478
+ FOREIGN KEY (`head_repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action,
479
+ FOREIGN KEY (`merged_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
480
+ FOREIGN KEY (`closed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
481
+);
482
+--> statement-breakpoint
483
+CREATE TABLE `scheduled_workflows` (
484
+ `id` text PRIMARY KEY NOT NULL,
485
+ `workflow_id` text NOT NULL,
486
+ `cron_expression` text NOT NULL,
487
+ `timezone` text DEFAULT 'UTC',
488
+ `is_enabled` integer DEFAULT true,
489
+ `last_run_at` text,
490
+ `next_run_at` text,
491
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
492
+ FOREIGN KEY (`workflow_id`) REFERENCES `workflows`(`id`) ON UPDATE no action ON DELETE cascade
493
+);
494
+--> statement-breakpoint
495
+CREATE TABLE `workflow_artifacts` (
496
+ `id` text PRIMARY KEY NOT NULL,
497
+ `run_id` text NOT NULL,
498
+ `job_id` text,
499
+ `name` text NOT NULL,
500
+ `size_bytes` integer NOT NULL,
501
+ `storage_path` text NOT NULL,
502
+ `mime_type` text,
503
+ `download_count` integer DEFAULT 0,
504
+ `expires_at` text,
505
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
506
+ FOREIGN KEY (`run_id`) REFERENCES `workflow_runs`(`id`) ON UPDATE no action ON DELETE cascade,
507
+ FOREIGN KEY (`job_id`) REFERENCES `workflow_jobs`(`id`) ON UPDATE no action ON DELETE no action
508
+);
509
+--> statement-breakpoint
510
+CREATE TABLE `workflow_jobs` (
511
+ `id` text PRIMARY KEY NOT NULL,
512
+ `run_id` text NOT NULL,
513
+ `name` text NOT NULL,
514
+ `status` text DEFAULT 'queued' NOT NULL,
515
+ `conclusion` text,
516
+ `runner_id` text,
517
+ `runner_name` text,
518
+ `runner_group_id` text,
519
+ `runner_group_name` text,
520
+ `container_id` text,
521
+ `container_image` text,
522
+ `needs` text,
523
+ `environment` text,
524
+ `environment_url` text,
525
+ `matrix` text,
526
+ `started_at` text,
527
+ `completed_at` text,
528
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
529
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
530
+ FOREIGN KEY (`run_id`) REFERENCES `workflow_runs`(`id`) ON UPDATE no action ON DELETE cascade
531
+);
532
+--> statement-breakpoint
533
+CREATE TABLE `workflow_logs` (
534
+ `id` text PRIMARY KEY NOT NULL,
535
+ `job_id` text NOT NULL,
536
+ `step_id` text,
537
+ `log_level` text DEFAULT 'info' NOT NULL,
538
+ `message` text NOT NULL,
539
+ `timestamp` text NOT NULL,
540
+ `line_number` integer,
541
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
542
+ FOREIGN KEY (`job_id`) REFERENCES `workflow_jobs`(`id`) ON UPDATE no action ON DELETE cascade,
543
+ FOREIGN KEY (`step_id`) REFERENCES `workflow_steps`(`id`) ON UPDATE no action ON DELETE no action
544
+);
545
+--> statement-breakpoint
546
+CREATE TABLE `workflow_runs` (
547
+ `id` text PRIMARY KEY NOT NULL,
548
+ `workflow_id` text NOT NULL,
549
+ `repository_id` text NOT NULL,
550
+ `run_number` integer NOT NULL,
551
+ `run_attempt` integer DEFAULT 1 NOT NULL,
552
+ `name` text NOT NULL,
553
+ `display_title` text,
554
+ `status` text DEFAULT 'queued' NOT NULL,
555
+ `conclusion` text,
556
+ `event` text NOT NULL,
557
+ `head_branch` text,
558
+ `head_sha` text NOT NULL,
559
+ `base_branch` text,
560
+ `base_sha` text,
561
+ `pull_request_id` text,
562
+ `triggered_by_id` text,
563
+ `started_at` text,
564
+ `completed_at` text,
565
+ `workflow_config` text,
566
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
567
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
568
+ FOREIGN KEY (`workflow_id`) REFERENCES `workflows`(`id`) ON UPDATE no action ON DELETE cascade,
569
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
570
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE no action,
571
+ FOREIGN KEY (`triggered_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
572
+);
573
+--> statement-breakpoint
574
+CREATE TABLE `workflow_secrets` (
575
+ `id` text PRIMARY KEY NOT NULL,
576
+ `repository_id` text NOT NULL,
577
+ `name` text NOT NULL,
578
+ `encrypted_value` text NOT NULL,
579
+ `environment` text,
580
+ `created_by_id` text,
581
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
582
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
583
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
584
+ FOREIGN KEY (`created_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
585
+);
586
+--> statement-breakpoint
587
+CREATE TABLE `workflow_steps` (
588
+ `id` text PRIMARY KEY NOT NULL,
589
+ `job_id` text NOT NULL,
590
+ `number` integer NOT NULL,
591
+ `name` text NOT NULL,
592
+ `status` text DEFAULT 'queued' NOT NULL,
593
+ `conclusion` text,
594
+ `uses` text,
595
+ `run` text,
596
+ `shell` text,
597
+ `working_directory` text,
598
+ `env` text,
599
+ `with` text,
600
+ `if` text,
601
+ `continue_on_error` integer DEFAULT false,
602
+ `timeout_minutes` integer,
603
+ `started_at` text,
604
+ `completed_at` text,
605
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
606
+ FOREIGN KEY (`job_id`) REFERENCES `workflow_jobs`(`id`) ON UPDATE no action ON DELETE cascade
607
+);
608
+--> statement-breakpoint
609
+CREATE TABLE `workflow_variables` (
610
+ `id` text PRIMARY KEY NOT NULL,
611
+ `repository_id` text NOT NULL,
612
+ `name` text NOT NULL,
613
+ `value` text NOT NULL,
614
+ `environment` text,
615
+ `created_by_id` text,
616
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
617
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
618
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
619
+ FOREIGN KEY (`created_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
620
+);
621
+--> statement-breakpoint
622
+CREATE TABLE `workflows` (
623
+ `id` text PRIMARY KEY NOT NULL,
624
+ `repository_id` text NOT NULL,
625
+ `name` text NOT NULL,
626
+ `path` text NOT NULL,
627
+ `state` text DEFAULT 'active' NOT NULL,
628
+ `badge_url` text,
629
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
630
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
631
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
632
+);
633
+--> statement-breakpoint
634
+CREATE TABLE `wiki_pages` (
635
+ `id` text PRIMARY KEY NOT NULL,
636
+ `repository_id` text NOT NULL,
637
+ `slug` text NOT NULL,
638
+ `title` text NOT NULL,
639
+ `content` text NOT NULL,
640
+ `format` text DEFAULT 'markdown' NOT NULL,
641
+ `parent_id` text,
642
+ `order` integer DEFAULT 0,
643
+ `last_editor_id` text,
644
+ `view_count` integer DEFAULT 0,
645
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
646
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
647
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
648
+ FOREIGN KEY (`parent_id`) REFERENCES `wiki_pages`(`id`) ON UPDATE no action ON DELETE no action,
649
+ FOREIGN KEY (`last_editor_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
650
+);
651
+--> statement-breakpoint
652
+CREATE TABLE `wiki_revisions` (
653
+ `id` text PRIMARY KEY NOT NULL,
654
+ `page_id` text NOT NULL,
655
+ `content` text NOT NULL,
656
+ `message` text,
657
+ `author_id` text NOT NULL,
658
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
659
+ FOREIGN KEY (`page_id`) REFERENCES `wiki_pages`(`id`) ON UPDATE no action ON DELETE cascade,
660
+ FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
661
+);
662
+--> statement-breakpoint
663
+CREATE TABLE `organization_members` (
664
+ `id` text PRIMARY KEY NOT NULL,
665
+ `organization_id` text NOT NULL,
666
+ `user_id` text NOT NULL,
667
+ `role` text DEFAULT 'member' NOT NULL,
668
+ `joined_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
669
+ FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade,
670
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
671
+);
672
+--> statement-breakpoint
673
+CREATE TABLE `organizations` (
674
+ `id` text PRIMARY KEY NOT NULL,
675
+ `name` text NOT NULL,
676
+ `display_name` text,
677
+ `description` text,
678
+ `avatar_url` text,
679
+ `website` text,
680
+ `location` text,
681
+ `email` text,
682
+ `is_verified` integer DEFAULT false,
683
+ `visibility` text DEFAULT 'public' NOT NULL,
684
+ `default_repo_permission` text DEFAULT 'read',
685
+ `members_can_create_repos` integer DEFAULT true,
686
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
687
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL
688
+);
689
+--> statement-breakpoint
690
+CREATE UNIQUE INDEX `organizations_name_unique` ON `organizations` (`name`);--> statement-breakpoint
691
+CREATE TABLE `team_members` (
692
+ `id` text PRIMARY KEY NOT NULL,
693
+ `team_id` text NOT NULL,
694
+ `user_id` text NOT NULL,
695
+ `role` text DEFAULT 'member' NOT NULL,
696
+ `joined_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
697
+ FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE cascade,
698
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
699
+);
700
+--> statement-breakpoint
701
+CREATE TABLE `team_repositories` (
702
+ `id` text PRIMARY KEY NOT NULL,
703
+ `team_id` text NOT NULL,
704
+ `repository_id` text NOT NULL,
705
+ `permission` text DEFAULT 'read' NOT NULL,
706
+ `added_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
707
+ FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE cascade,
708
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
709
+);
710
+--> statement-breakpoint
711
+CREATE TABLE `teams` (
712
+ `id` text PRIMARY KEY NOT NULL,
713
+ `organization_id` text NOT NULL,
714
+ `name` text NOT NULL,
715
+ `slug` text NOT NULL,
716
+ `description` text,
717
+ `privacy` text DEFAULT 'visible' NOT NULL,
718
+ `parent_id` text,
719
+ `permission` text DEFAULT 'read' NOT NULL,
720
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
721
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
722
+ FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade,
723
+ FOREIGN KEY (`parent_id`) REFERENCES `teams`(`id`) ON UPDATE no action ON DELETE no action
724
+);
725
+--> statement-breakpoint
726
+CREATE TABLE `activities` (
727
+ `id` text PRIMARY KEY NOT NULL,
728
+ `user_id` text NOT NULL,
729
+ `repository_id` text,
730
+ `organization_id` text,
731
+ `type` text NOT NULL,
732
+ `action` text NOT NULL,
733
+ `ref_type` text,
734
+ `ref_name` text,
735
+ `target_type` text,
736
+ `target_id` text,
737
+ `payload` text,
738
+ `is_public` integer DEFAULT true,
739
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
740
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
741
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
742
+ FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action
743
+);
744
+--> statement-breakpoint
745
+CREATE TABLE `audit_logs` (
746
+ `id` text PRIMARY KEY NOT NULL,
747
+ `user_id` text,
748
+ `organization_id` text,
749
+ `repository_id` text,
750
+ `action` text NOT NULL,
751
+ `actor_type` text DEFAULT 'user' NOT NULL,
752
+ `actor_id` text,
753
+ `actor_ip` text,
754
+ `actor_user_agent` text,
755
+ `target_type` text,
756
+ `target_id` text,
757
+ `target_name` text,
758
+ `data` text,
759
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
760
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
761
+ FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action,
762
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action
763
+);
764
+--> statement-breakpoint
765
+CREATE TABLE `notifications` (
766
+ `id` text PRIMARY KEY NOT NULL,
767
+ `user_id` text NOT NULL,
768
+ `repository_id` text,
769
+ `type` text NOT NULL,
770
+ `title` text NOT NULL,
771
+ `body` text,
772
+ `url` text,
773
+ `actor_id` text,
774
+ `subject_type` text,
775
+ `subject_id` text,
776
+ `reason` text NOT NULL,
777
+ `is_read` integer DEFAULT false,
778
+ `read_at` text,
779
+ `is_archived` integer DEFAULT false,
780
+ `archived_at` text,
781
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
782
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
783
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
784
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE no action,
785
+ FOREIGN KEY (`actor_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
786
+);
787
+--> statement-breakpoint
788
+CREATE TABLE `security_scans` (
789
+ `id` text PRIMARY KEY NOT NULL,
790
+ `repository_id` text NOT NULL,
791
+ `status` text DEFAULT 'queued' NOT NULL,
792
+ `started_at` integer,
793
+ `completed_at` integer,
794
+ `critical_count` integer DEFAULT 0,
795
+ `high_count` integer DEFAULT 0,
796
+ `medium_count` integer DEFAULT 0,
797
+ `low_count` integer DEFAULT 0,
798
+ `unknown_count` integer DEFAULT 0,
799
+ `logs` text,
800
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
801
+);
802
+--> statement-breakpoint
803
+CREATE TABLE `security_vulnerabilities` (
804
+ `id` text PRIMARY KEY NOT NULL,
805
+ `scan_id` text NOT NULL,
806
+ `vulnerability_id` text NOT NULL,
807
+ `pkg_name` text NOT NULL,
808
+ `installed_version` text,
809
+ `fixed_version` text,
810
+ `severity` text NOT NULL,
811
+ `title` text,
812
+ `description` text,
813
+ `target` text,
814
+ `class` text,
815
+ FOREIGN KEY (`scan_id`) REFERENCES `security_scans`(`id`) ON UPDATE no action ON DELETE cascade
816
+);
817
+--> statement-breakpoint
818
+CREATE TABLE `pipeline_runners` (
819
+ `id` text PRIMARY KEY NOT NULL,
820
+ `repository_id` text,
821
+ `token` text NOT NULL,
822
+ `name` text NOT NULL,
823
+ `os` text,
824
+ `arch` text,
825
+ `version` text,
826
+ `status` text DEFAULT 'offline' NOT NULL,
827
+ `last_seen_at` text,
828
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
829
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
830
+);
831
+--> statement-breakpoint
832
+CREATE TABLE `pr_stack_entries` (
833
+ `id` text PRIMARY KEY NOT NULL,
834
+ `stack_id` text NOT NULL,
835
+ `pull_request_id` text NOT NULL,
836
+ `stack_order` integer NOT NULL,
837
+ `parent_pr_id` text,
838
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
839
+ FOREIGN KEY (`stack_id`) REFERENCES `pr_stacks`(`id`) ON UPDATE no action ON DELETE cascade,
840
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
841
+ FOREIGN KEY (`parent_pr_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE no action
842
+);
843
+--> statement-breakpoint
844
+CREATE TABLE `pr_stacks` (
845
+ `id` text PRIMARY KEY NOT NULL,
846
+ `repository_id` text NOT NULL,
847
+ `name` text,
848
+ `base_branch` text NOT NULL,
849
+ `status` text DEFAULT 'active' NOT NULL,
850
+ `created_by_id` text NOT NULL,
851
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
852
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
853
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
854
+ FOREIGN KEY (`created_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
855
+);
856
+--> statement-breakpoint
857
+CREATE TABLE `merge_queue` (
858
+ `id` text PRIMARY KEY NOT NULL,
859
+ `repository_id` text NOT NULL,
860
+ `pull_request_id` text NOT NULL,
861
+ `stack_id` text,
862
+ `status` text DEFAULT 'pending' NOT NULL,
863
+ `priority` integer DEFAULT 0 NOT NULL,
864
+ `position` integer,
865
+ `ci_status` text DEFAULT 'pending',
866
+ `ci_run_id` text,
867
+ `added_by_id` text NOT NULL,
868
+ `added_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
869
+ `estimated_merge_at` text,
870
+ `started_at` text,
871
+ `completed_at` text,
872
+ `failure_reason` text,
873
+ `retry_count` integer DEFAULT 0,
874
+ `merge_method` text DEFAULT 'merge',
875
+ `delete_on_merge` integer DEFAULT true,
876
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
877
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
878
+ FOREIGN KEY (`stack_id`) REFERENCES `pr_stacks`(`id`) ON UPDATE no action ON DELETE no action,
879
+ FOREIGN KEY (`added_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
880
+);
881
+--> statement-breakpoint
882
+CREATE TABLE `ai_review_suggestions` (
883
+ `id` text PRIMARY KEY NOT NULL,
884
+ `ai_review_id` text NOT NULL,
885
+ `path` text NOT NULL,
886
+ `line` integer,
887
+ `end_line` integer,
888
+ `severity` text NOT NULL,
889
+ `type` text NOT NULL,
890
+ `category` text,
891
+ `title` text NOT NULL,
892
+ `message` text NOT NULL,
893
+ `suggested_fix` text,
894
+ `explanation` text,
895
+ `is_applied` integer DEFAULT false,
896
+ `is_dismissed` integer DEFAULT false,
897
+ `applied_at` text,
898
+ `applied_by_id` text,
899
+ `dismissed_at` text,
900
+ `dismissed_by_id` text,
901
+ `dismiss_reason` text,
902
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
903
+ FOREIGN KEY (`ai_review_id`) REFERENCES `ai_reviews`(`id`) ON UPDATE no action ON DELETE cascade,
904
+ FOREIGN KEY (`applied_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
905
+ FOREIGN KEY (`dismissed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
906
+);
907
+--> statement-breakpoint
908
+CREATE TABLE `ai_reviews` (
909
+ `id` text PRIMARY KEY NOT NULL,
910
+ `pull_request_id` text NOT NULL,
911
+ `status` text DEFAULT 'pending' NOT NULL,
912
+ `model` text NOT NULL,
913
+ `provider` text NOT NULL,
914
+ `stack_context` text,
915
+ `includes_stack_context` integer DEFAULT false,
916
+ `summary` text,
917
+ `overall_severity` text,
918
+ `suggestions_count` integer DEFAULT 0,
919
+ `tokens_used` integer,
920
+ `prompt_tokens` integer,
921
+ `completion_tokens` integer,
922
+ `cost_cents` integer,
923
+ `raw_response` text,
924
+ `triggered_by_id` text,
925
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
926
+ `started_at` text,
927
+ `completed_at` text,
928
+ `error_message` text,
929
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
930
+ FOREIGN KEY (`triggered_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
931
+);
932
+--> statement-breakpoint
933
+CREATE TABLE `pr_metrics` (
934
+ `id` text PRIMARY KEY NOT NULL,
935
+ `pull_request_id` text NOT NULL,
936
+ `repository_id` text NOT NULL,
937
+ `author_id` text NOT NULL,
938
+ `time_to_first_review` integer,
939
+ `time_to_approval` integer,
940
+ `time_to_merge` integer,
941
+ `total_cycle_time` integer,
942
+ `review_rounds` integer DEFAULT 1,
943
+ `reviewers_count` integer DEFAULT 0,
944
+ `comments_count` integer DEFAULT 0,
945
+ `changes_requested_count` integer DEFAULT 0,
946
+ `lines_added` integer DEFAULT 0,
947
+ `lines_removed` integer DEFAULT 0,
948
+ `files_changed` integer DEFAULT 0,
949
+ `commits` integer DEFAULT 1,
950
+ `is_stacked` integer DEFAULT false,
951
+ `stack_position` integer,
952
+ `pr_created_at` text,
953
+ `first_review_at` text,
954
+ `approved_at` text,
955
+ `merged_at` text,
956
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
957
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
958
+ FOREIGN KEY (`pull_request_id`) REFERENCES `pull_requests`(`id`) ON UPDATE no action ON DELETE cascade,
959
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
960
+ FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
961
+);
962
+--> statement-breakpoint
963
+CREATE TABLE `repo_metrics` (
964
+ `id` text PRIMARY KEY NOT NULL,
965
+ `repository_id` text NOT NULL,
966
+ `week_of` text NOT NULL,
967
+ `prs_opened` integer DEFAULT 0,
968
+ `prs_merged` integer DEFAULT 0,
969
+ `prs_closed` integer DEFAULT 0,
970
+ `avg_time_to_first_review` integer,
971
+ `avg_time_to_merge` integer,
972
+ `avg_review_rounds` integer,
973
+ `avg_lines_changed` integer,
974
+ `avg_files_changed` integer,
975
+ `stacked_prs` integer DEFAULT 0,
976
+ `stacked_prs_percentage` integer DEFAULT 0,
977
+ `active_authors` integer DEFAULT 0,
978
+ `active_reviewers` integer DEFAULT 0,
979
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
980
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
981
+);
982
+--> statement-breakpoint
983
+CREATE TABLE `review_metrics` (
984
+ `id` text PRIMARY KEY NOT NULL,
985
+ `user_id` text NOT NULL,
986
+ `repository_id` text,
987
+ `organization_id` text,
988
+ `week_of` text NOT NULL,
989
+ `prs_authored` integer DEFAULT 0,
990
+ `prs_authored_merged` integer DEFAULT 0,
991
+ `avg_time_to_merge_authored` integer,
992
+ `lines_authored_added` integer DEFAULT 0,
993
+ `lines_authored_removed` integer DEFAULT 0,
994
+ `prs_reviewed` integer DEFAULT 0,
995
+ `avg_time_to_review` integer,
996
+ `comments_given` integer DEFAULT 0,
997
+ `approvals_given` integer DEFAULT 0,
998
+ `changes_requested_given` integer DEFAULT 0,
999
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
1000
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
1001
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
1002
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade,
1003
+ FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE no action
1004
+);
1005
+--> statement-breakpoint
1006
+CREATE TABLE `slack_channel_mappings` (
1007
+ `id` text PRIMARY KEY NOT NULL,
1008
+ `workspace_id` text NOT NULL,
1009
+ `repository_id` text NOT NULL,
1010
+ `channel_id` text NOT NULL,
1011
+ `channel_name` text,
1012
+ `notify_on` text DEFAULT '["pr_created","pr_merged","review_requested","ci_failed"]',
1013
+ `notify_branches` text,
1014
+ `notify_authors` text,
1015
+ `is_active` integer DEFAULT true,
1016
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
1017
+ `updated_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
1018
+ FOREIGN KEY (`workspace_id`) REFERENCES `slack_workspaces`(`id`) ON UPDATE no action ON DELETE cascade,
1019
+ FOREIGN KEY (`repository_id`) REFERENCES `repositories`(`id`) ON UPDATE no action ON DELETE cascade
1020
+);
1021
+--> statement-breakpoint
1022
+CREATE TABLE `slack_user_mappings` (
1023
+ `id` text PRIMARY KEY NOT NULL,
1024
+ `user_id` text NOT NULL,
1025
+ `workspace_id` text NOT NULL,
1026
+ `slack_user_id` text NOT NULL,
1027
+ `slack_username` text,
1028
+ `dm_preferences` text DEFAULT '{"review_requested":true,"pr_approved":true,"ci_failed":true}',
1029
+ `dnd_enabled` integer DEFAULT false,
1030
+ `dnd_start` text,
1031
+ `dnd_end` text,
1032
+ `dnd_timezone` text,
1033
+ `is_active` integer DEFAULT true,
1034
+ `created_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
1035
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
1036
+ FOREIGN KEY (`workspace_id`) REFERENCES `slack_workspaces`(`id`) ON UPDATE no action ON DELETE cascade
1037
+);
1038
+--> statement-breakpoint
1039
+CREATE TABLE `slack_workspaces` (
1040
+ `id` text PRIMARY KEY NOT NULL,
1041
+ `organization_id` text,
1042
+ `team_id` text NOT NULL,
1043
+ `team_name` text,
1044
+ `team_domain` text,
1045
+ `access_token` text NOT NULL,
1046
+ `bot_user_id` text,
1047
+ `bot_access_token` text,
1048
+ `scopes` text,
1049
+ `installed_by_id` text NOT NULL,
1050
+ `installed_at` text DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,
1051
+ `is_active` integer DEFAULT true,
1052
+ `last_used_at` text,
1053
+ FOREIGN KEY (`organization_id`) REFERENCES `organizations`(`id`) ON UPDATE no action ON DELETE cascade,
1054
+ FOREIGN KEY (`installed_by_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
1055
+);
0
1056
▼package-lock.json
+1869-125
42
42
"@radix-ui/react-toggle": "^1.1.0",
43
43
"@radix-ui/react-toggle-group": "^1.1.0",
44
44
"@radix-ui/react-tooltip": "^1.1.2",
45
+ "@types/nodemailer": "^7.0.4",
45
46
"astro": "^4.11.0",
46
47
"bcryptjs": "^2.4.3",
47
48
"better-sqlite3": "^11.1.2",
49
50
"class-variance-authority": "^0.7.0",
50
51
"clsx": "^2.1.1",
51
52
"cmdk": "^1.1.1",
53
+ "cobe": "^0.6.5",
52
54
"dockerode": "^4.0.2",
53
55
"drizzle-orm": "^0.45.1",
54
- "framer-motion": "^11.2.12",
56
+ "framer-motion": "^11.18.2",
55
57
"github-markdown-css": "^5.8.1",
58
+ "googleapis": "^169.0.0",
56
59
"ioredis": "^5.8.2",
57
60
"isomorphic-git": "^1.25.10",
58
61
"jose": "^5.6.3",
63
66
"mysql2": "^3.15.3",
64
67
"nanoid": "^5.0.7",
65
68
"nodegit": "^0.28.0-alpha.27",
69
+ "nodemailer": "^7.0.12",
70
+ "openpgp": "^6.3.0",
66
71
"otplib": "^12.0.1",
67
72
"pg": "^8.16.3",
68
73
"pino": "^10.1.0",
70
75
"prom-client": "^15.1.3",
71
76
"react": "^18.3.1",
72
77
"react-dom": "^18.3.1",
78
+ "react-globe.gl": "^2.37.0",
73
79
"rehype-sanitize": "^6.0.0",
74
80
"rehype-stringify": "^10.0.1",
75
81
"remark-gfm": "^4.0.1",
77
83
"remark-rehype": "^11.1.2",
78
84
"simple-git": "^3.25.0",
79
85
"ssh2": "^1.15.0",
80
- "tailwind-merge": "^2.3.0",
86
+ "tailwind-merge": "^2.6.0",
81
87
"tailwindcss-animate": "^1.0.7",
88
+ "three": "^0.182.0",
82
89
"unified": "^11.0.5",
83
90
"zod": "^3.23.8",
84
91
"zustand": "^4.5.4"
85
92
},
86
93
"devDependencies": {
94
+ "@faker-js/faker": "^10.1.0",
87
95
"@playwright/test": "^1.57.0",
88
96
"@tailwindcss/typography": "^0.5.19",
89
97
"@types/bcryptjs": "^2.4.6",
91
99
"@types/dockerode": "^3.3.29",
92
100
"@types/js-yaml": "^4.0.9",
93
101
"@types/node": "^20.14.9",
94
- "@types/pg": "^8.11.6",
102
+ "@types/pg": "^8.16.0",
95
103
"@types/react": "^18.3.3",
96
104
"@types/react-dom": "^18.3.0",
97
105
"@types/ssh2": "^1.15.0",
106
+ "@types/three": "^0.182.0",
98
107
"autoprefixer": "^10.4.19",
99
108
"drizzle-kit": "^0.31.8",
100
109
"pino-pretty": "^13.1.3",
943
952
"node": ">=18.0.0"
944
953
}
945
954
},
955
+ "node_modules/@aws-sdk/client-sesv2": {
956
+ "version": "3.958.0",
957
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.958.0.tgz",
958
+ "integrity": "sha512-3x3n8IIxIMAkdpt9wy9zS7MO2lqTcJwQTdHMn6BlD7YUohb+r5Q4KCOEQ2uHWd4WIJv2tlbXnfypHaXReO/WXA==",
959
+ "license": "Apache-2.0",
960
+ "dependencies": {
961
+ "@aws-crypto/sha256-browser": "5.2.0",
962
+ "@aws-crypto/sha256-js": "5.2.0",
963
+ "@aws-sdk/core": "3.957.0",
964
+ "@aws-sdk/credential-provider-node": "3.958.0",
965
+ "@aws-sdk/middleware-host-header": "3.957.0",
966
+ "@aws-sdk/middleware-logger": "3.957.0",
967
+ "@aws-sdk/middleware-recursion-detection": "3.957.0",
968
+ "@aws-sdk/middleware-user-agent": "3.957.0",
969
+ "@aws-sdk/region-config-resolver": "3.957.0",
970
+ "@aws-sdk/signature-v4-multi-region": "3.957.0",
971
+ "@aws-sdk/types": "3.957.0",
972
+ "@aws-sdk/util-endpoints": "3.957.0",
973
+ "@aws-sdk/util-user-agent-browser": "3.957.0",
974
+ "@aws-sdk/util-user-agent-node": "3.957.0",
975
+ "@smithy/config-resolver": "^4.4.5",
976
+ "@smithy/core": "^3.20.0",
977
+ "@smithy/fetch-http-handler": "^5.3.8",
978
+ "@smithy/hash-node": "^4.2.7",
979
+ "@smithy/invalid-dependency": "^4.2.7",
980
+ "@smithy/middleware-content-length": "^4.2.7",
981
+ "@smithy/middleware-endpoint": "^4.4.1",
982
+ "@smithy/middleware-retry": "^4.4.17",
983
+ "@smithy/middleware-serde": "^4.2.8",
984
+ "@smithy/middleware-stack": "^4.2.7",
985
+ "@smithy/node-config-provider": "^4.3.7",
986
+ "@smithy/node-http-handler": "^4.4.7",
987
+ "@smithy/protocol-http": "^5.3.7",
988
+ "@smithy/smithy-client": "^4.10.2",
989
+ "@smithy/types": "^4.11.0",
990
+ "@smithy/url-parser": "^4.2.7",
991
+ "@smithy/util-base64": "^4.3.0",
992
+ "@smithy/util-body-length-browser": "^4.2.0",
993
+ "@smithy/util-body-length-node": "^4.2.1",
994
+ "@smithy/util-defaults-mode-browser": "^4.3.16",
995
+ "@smithy/util-defaults-mode-node": "^4.2.19",
996
+ "@smithy/util-endpoints": "^3.2.7",
997
+ "@smithy/util-middleware": "^4.2.7",
998
+ "@smithy/util-retry": "^4.2.7",
999
+ "@smithy/util-utf8": "^4.2.0",
1000
+ "tslib": "^2.6.2"
1001
+ },
1002
+ "engines": {
1003
+ "node": ">=18.0.0"
1004
+ }
1005
+ },
1006
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": {
1007
+ "version": "3.958.0",
1008
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.958.0.tgz",
1009
+ "integrity": "sha512-6qNCIeaMzKzfqasy2nNRuYnMuaMebCcCPP4J2CVGkA8QYMbIVKPlkn9bpB20Vxe6H/r3jtCCLQaOJjVTx/6dXg==",
1010
+ "license": "Apache-2.0",
1011
+ "dependencies": {
1012
+ "@aws-crypto/sha256-browser": "5.2.0",
1013
+ "@aws-crypto/sha256-js": "5.2.0",
1014
+ "@aws-sdk/core": "3.957.0",
1015
+ "@aws-sdk/middleware-host-header": "3.957.0",
1016
+ "@aws-sdk/middleware-logger": "3.957.0",
1017
+ "@aws-sdk/middleware-recursion-detection": "3.957.0",
1018
+ "@aws-sdk/middleware-user-agent": "3.957.0",
1019
+ "@aws-sdk/region-config-resolver": "3.957.0",
1020
+ "@aws-sdk/types": "3.957.0",
1021
+ "@aws-sdk/util-endpoints": "3.957.0",
1022
+ "@aws-sdk/util-user-agent-browser": "3.957.0",
1023
+ "@aws-sdk/util-user-agent-node": "3.957.0",
1024
+ "@smithy/config-resolver": "^4.4.5",
1025
+ "@smithy/core": "^3.20.0",
1026
+ "@smithy/fetch-http-handler": "^5.3.8",
1027
+ "@smithy/hash-node": "^4.2.7",
1028
+ "@smithy/invalid-dependency": "^4.2.7",
1029
+ "@smithy/middleware-content-length": "^4.2.7",
1030
+ "@smithy/middleware-endpoint": "^4.4.1",
1031
+ "@smithy/middleware-retry": "^4.4.17",
1032
+ "@smithy/middleware-serde": "^4.2.8",
1033
+ "@smithy/middleware-stack": "^4.2.7",
1034
+ "@smithy/node-config-provider": "^4.3.7",
1035
+ "@smithy/node-http-handler": "^4.4.7",
1036
+ "@smithy/protocol-http": "^5.3.7",
1037
+ "@smithy/smithy-client": "^4.10.2",
1038
+ "@smithy/types": "^4.11.0",
1039
+ "@smithy/url-parser": "^4.2.7",
1040
+ "@smithy/util-base64": "^4.3.0",
1041
+ "@smithy/util-body-length-browser": "^4.2.0",
1042
+ "@smithy/util-body-length-node": "^4.2.1",
1043
+ "@smithy/util-defaults-mode-browser": "^4.3.16",
1044
+ "@smithy/util-defaults-mode-node": "^4.2.19",
1045
+ "@smithy/util-endpoints": "^3.2.7",
1046
+ "@smithy/util-middleware": "^4.2.7",
1047
+ "@smithy/util-retry": "^4.2.7",
1048
+ "@smithy/util-utf8": "^4.2.0",
1049
+ "tslib": "^2.6.2"
1050
+ },
1051
+ "engines": {
1052
+ "node": ">=18.0.0"
1053
+ }
1054
+ },
1055
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": {
1056
+ "version": "3.957.0",
1057
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.957.0.tgz",
1058
+ "integrity": "sha512-DrZgDnF1lQZv75a52nFWs6MExihJF2GZB6ETZRqr6jMwhrk2kbJPUtvgbifwcL7AYmVqHQDJBrR/MqkwwFCpiw==",
1059
+ "license": "Apache-2.0",
1060
+ "dependencies": {
1061
+ "@aws-sdk/types": "3.957.0",
1062
+ "@aws-sdk/xml-builder": "3.957.0",
1063
+ "@smithy/core": "^3.20.0",
1064
+ "@smithy/node-config-provider": "^4.3.7",
1065
+ "@smithy/property-provider": "^4.2.7",
1066
+ "@smithy/protocol-http": "^5.3.7",
1067
+ "@smithy/signature-v4": "^5.3.7",
1068
+ "@smithy/smithy-client": "^4.10.2",
1069
+ "@smithy/types": "^4.11.0",
1070
+ "@smithy/util-base64": "^4.3.0",
1071
+ "@smithy/util-middleware": "^4.2.7",
1072
+ "@smithy/util-utf8": "^4.2.0",
1073
+ "tslib": "^2.6.2"
1074
+ },
1075
+ "engines": {
1076
+ "node": ">=18.0.0"
1077
+ }
1078
+ },
1079
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": {
1080
+ "version": "3.957.0",
1081
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.957.0.tgz",
1082
+ "integrity": "sha512-475mkhGaWCr+Z52fOOVb/q2VHuNvqEDixlYIkeaO6xJ6t9qR0wpLt4hOQaR6zR1wfZV0SlE7d8RErdYq/PByog==",
1083
+ "license": "Apache-2.0",
1084
+ "dependencies": {
1085
+ "@aws-sdk/core": "3.957.0",
1086
+ "@aws-sdk/types": "3.957.0",
1087
+ "@smithy/property-provider": "^4.2.7",
1088
+ "@smithy/types": "^4.11.0",
1089
+ "tslib": "^2.6.2"
1090
+ },
1091
+ "engines": {
1092
+ "node": ">=18.0.0"
1093
+ }
1094
+ },
1095
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": {
1096
+ "version": "3.957.0",
1097
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.957.0.tgz",
1098
+ "integrity": "sha512-8dS55QHRxXgJlHkEYaCGZIhieCs9NU1HU1BcqQ4RfUdSsfRdxxktqUKgCnBnOOn0oD3PPA8cQOCAVgIyRb3Rfw==",
1099
+ "license": "Apache-2.0",
1100
+ "dependencies": {
1101
+ "@aws-sdk/core": "3.957.0",
1102
+ "@aws-sdk/types": "3.957.0",
1103
+ "@smithy/fetch-http-handler": "^5.3.8",
1104
+ "@smithy/node-http-handler": "^4.4.7",
1105
+ "@smithy/property-provider": "^4.2.7",
1106
+ "@smithy/protocol-http": "^5.3.7",
1107
+ "@smithy/smithy-client": "^4.10.2",
1108
+ "@smithy/types": "^4.11.0",
1109
+ "@smithy/util-stream": "^4.5.8",
1110
+ "tslib": "^2.6.2"
1111
+ },
1112
+ "engines": {
1113
+ "node": ">=18.0.0"
1114
+ }
1115
+ },
1116
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": {
1117
+ "version": "3.958.0",
1118
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.958.0.tgz",
1119
+ "integrity": "sha512-u7twvZa1/6GWmPBZs6DbjlegCoNzNjBsMS/6fvh5quByYrcJr/uLd8YEr7S3UIq4kR/gSnHqcae7y2nL2bqZdg==",
1120
+ "license": "Apache-2.0",
1121
+ "dependencies": {
1122
+ "@aws-sdk/core": "3.957.0",
1123
+ "@aws-sdk/credential-provider-env": "3.957.0",
1124
+ "@aws-sdk/credential-provider-http": "3.957.0",
1125
+ "@aws-sdk/credential-provider-login": "3.958.0",
1126
+ "@aws-sdk/credential-provider-process": "3.957.0",
1127
+ "@aws-sdk/credential-provider-sso": "3.958.0",
1128
+ "@aws-sdk/credential-provider-web-identity": "3.958.0",
1129
+ "@aws-sdk/nested-clients": "3.958.0",
1130
+ "@aws-sdk/types": "3.957.0",
1131
+ "@smithy/credential-provider-imds": "^4.2.7",
1132
+ "@smithy/property-provider": "^4.2.7",
1133
+ "@smithy/shared-ini-file-loader": "^4.4.2",
1134
+ "@smithy/types": "^4.11.0",
1135
+ "tslib": "^2.6.2"
1136
+ },
1137
+ "engines": {
1138
+ "node": ">=18.0.0"
1139
+ }
1140
+ },
1141
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-login": {
1142
+ "version": "3.958.0",
1143
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.958.0.tgz",
1144
+ "integrity": "sha512-sDwtDnBSszUIbzbOORGh5gmXGl9aK25+BHb4gb1aVlqB+nNL2+IUEJA62+CE55lXSH8qXF90paivjK8tOHTwPA==",
1145
+ "license": "Apache-2.0",
1146
+ "dependencies": {
1147
+ "@aws-sdk/core": "3.957.0",
1148
+ "@aws-sdk/nested-clients": "3.958.0",
1149
+ "@aws-sdk/types": "3.957.0",
1150
+ "@smithy/property-provider": "^4.2.7",
1151
+ "@smithy/protocol-http": "^5.3.7",
1152
+ "@smithy/shared-ini-file-loader": "^4.4.2",
1153
+ "@smithy/types": "^4.11.0",
1154
+ "tslib": "^2.6.2"
1155
+ },
1156
+ "engines": {
1157
+ "node": ">=18.0.0"
1158
+ }
1159
+ },
1160
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": {
1161
+ "version": "3.958.0",
1162
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.958.0.tgz",
1163
+ "integrity": "sha512-vdoZbNG2dt66I7EpN3fKCzi6fp9xjIiwEA/vVVgqO4wXCGw8rKPIdDUus4e13VvTr330uQs2W0UNg/7AgtquEQ==",
1164
+ "license": "Apache-2.0",
1165
+ "dependencies": {
1166
+ "@aws-sdk/credential-provider-env": "3.957.0",
1167
+ "@aws-sdk/credential-provider-http": "3.957.0",
1168
+ "@aws-sdk/credential-provider-ini": "3.958.0",
1169
+ "@aws-sdk/credential-provider-process": "3.957.0",
1170
+ "@aws-sdk/credential-provider-sso": "3.958.0",
1171
+ "@aws-sdk/credential-provider-web-identity": "3.958.0",
1172
+ "@aws-sdk/types": "3.957.0",
1173
+ "@smithy/credential-provider-imds": "^4.2.7",
1174
+ "@smithy/property-provider": "^4.2.7",
1175
+ "@smithy/shared-ini-file-loader": "^4.4.2",
1176
+ "@smithy/types": "^4.11.0",
1177
+ "tslib": "^2.6.2"
1178
+ },
1179
+ "engines": {
1180
+ "node": ">=18.0.0"
1181
+ }
1182
+ },
1183
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": {
1184
+ "version": "3.957.0",
1185
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.957.0.tgz",
1186
+ "integrity": "sha512-/KIz9kadwbeLy6SKvT79W81Y+hb/8LMDyeloA2zhouE28hmne+hLn0wNCQXAAupFFlYOAtZR2NTBs7HBAReJlg==",
1187
+ "license": "Apache-2.0",
1188
+ "dependencies": {
1189
+ "@aws-sdk/core": "3.957.0",
1190
+ "@aws-sdk/types": "3.957.0",
1191
+ "@smithy/property-provider": "^4.2.7",
1192
+ "@smithy/shared-ini-file-loader": "^4.4.2",
1193
+ "@smithy/types": "^4.11.0",
1194
+ "tslib": "^2.6.2"
1195
+ },
1196
+ "engines": {
1197
+ "node": ">=18.0.0"
1198
+ }
1199
+ },
1200
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": {
1201
+ "version": "3.958.0",
1202
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.958.0.tgz",
1203
+ "integrity": "sha512-CBYHJ5ufp8HC4q+o7IJejCUctJXWaksgpmoFpXerbjAso7/Fg7LLUu9inXVOxlHKLlvYekDXjIUBXDJS2WYdgg==",
1204
+ "license": "Apache-2.0",
1205
+ "dependencies": {
1206
+ "@aws-sdk/client-sso": "3.958.0",
1207
+ "@aws-sdk/core": "3.957.0",
1208
+ "@aws-sdk/token-providers": "3.958.0",
1209
+ "@aws-sdk/types": "3.957.0",
1210
+ "@smithy/property-provider": "^4.2.7",
1211
+ "@smithy/shared-ini-file-loader": "^4.4.2",
1212
+ "@smithy/types": "^4.11.0",
1213
+ "tslib": "^2.6.2"
1214
+ },
1215
+ "engines": {
1216
+ "node": ">=18.0.0"
1217
+ }
1218
+ },
1219
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": {
1220
+ "version": "3.958.0",
1221
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.958.0.tgz",
1222
+ "integrity": "sha512-dgnvwjMq5Y66WozzUzxNkCFap+umHUtqMMKlr8z/vl9NYMLem/WUbWNpFFOVFWquXikc+ewtpBMR4KEDXfZ+KA==",
1223
+ "license": "Apache-2.0",
1224
+ "dependencies": {
1225
+ "@aws-sdk/core": "3.957.0",
1226
+ "@aws-sdk/nested-clients": "3.958.0",
1227
+ "@aws-sdk/types": "3.957.0",
1228
+ "@smithy/property-provider": "^4.2.7",
1229
+ "@smithy/shared-ini-file-loader": "^4.4.2",
1230
+ "@smithy/types": "^4.11.0",
1231
+ "tslib": "^2.6.2"
1232
+ },
1233
+ "engines": {
1234
+ "node": ">=18.0.0"
1235
+ }
1236
+ },
1237
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-host-header": {
1238
+ "version": "3.957.0",
1239
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.957.0.tgz",
1240
+ "integrity": "sha512-BBgKawVyfQZglEkNTuBBdC3azlyqNXsvvN4jPkWAiNYcY0x1BasaJFl+7u/HisfULstryweJq/dAvIZIxzlZaA==",
1241
+ "license": "Apache-2.0",
1242
+ "dependencies": {
1243
+ "@aws-sdk/types": "3.957.0",
1244
+ "@smithy/protocol-http": "^5.3.7",
1245
+ "@smithy/types": "^4.11.0",
1246
+ "tslib": "^2.6.2"
1247
+ },
1248
+ "engines": {
1249
+ "node": ">=18.0.0"
1250
+ }
1251
+ },
1252
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-logger": {
1253
+ "version": "3.957.0",
1254
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.957.0.tgz",
1255
+ "integrity": "sha512-w1qfKrSKHf9b5a8O76yQ1t69u6NWuBjr5kBX+jRWFx/5mu6RLpqERXRpVJxfosbep7k3B+DSB5tZMZ82GKcJtQ==",
1256
+ "license": "Apache-2.0",
1257
+ "dependencies": {
1258
+ "@aws-sdk/types": "3.957.0",
1259
+ "@smithy/types": "^4.11.0",
1260
+ "tslib": "^2.6.2"
1261
+ },
1262
+ "engines": {
1263
+ "node": ">=18.0.0"
1264
+ }
1265
+ },
1266
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-recursion-detection": {
1267
+ "version": "3.957.0",
1268
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.957.0.tgz",
1269
+ "integrity": "sha512-D2H/WoxhAZNYX+IjkKTdOhOkWQaK0jjJrDBj56hKjU5c9ltQiaX/1PqJ4dfjHntEshJfu0w+E6XJ+/6A6ILBBA==",
1270
+ "license": "Apache-2.0",
1271
+ "dependencies": {
1272
+ "@aws-sdk/types": "3.957.0",
1273
+ "@aws/lambda-invoke-store": "^0.2.2",
1274
+ "@smithy/protocol-http": "^5.3.7",
1275
+ "@smithy/types": "^4.11.0",
1276
+ "tslib": "^2.6.2"
1277
+ },
1278
+ "engines": {
1279
+ "node": ">=18.0.0"
1280
+ }
1281
+ },
1282
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": {
1283
+ "version": "3.957.0",
1284
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.957.0.tgz",
1285
+ "integrity": "sha512-5B2qY2nR2LYpxoQP0xUum5A1UNvH2JQpLHDH1nWFNF/XetV7ipFHksMxPNhtJJ6ARaWhQIDXfOUj0jcnkJxXUg==",
1286
+ "license": "Apache-2.0",
1287
+ "dependencies": {
1288
+ "@aws-sdk/core": "3.957.0",
1289
+ "@aws-sdk/types": "3.957.0",
1290
+ "@aws-sdk/util-arn-parser": "3.957.0",
1291
+ "@smithy/core": "^3.20.0",
1292
+ "@smithy/node-config-provider": "^4.3.7",
1293
+ "@smithy/protocol-http": "^5.3.7",
1294
+ "@smithy/signature-v4": "^5.3.7",
1295
+ "@smithy/smithy-client": "^4.10.2",
1296
+ "@smithy/types": "^4.11.0",
1297
+ "@smithy/util-config-provider": "^4.2.0",
1298
+ "@smithy/util-middleware": "^4.2.7",
1299
+ "@smithy/util-stream": "^4.5.8",
1300
+ "@smithy/util-utf8": "^4.2.0",
1301
+ "tslib": "^2.6.2"
1302
+ },
1303
+ "engines": {
1304
+ "node": ">=18.0.0"
1305
+ }
1306
+ },
1307
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": {
1308
+ "version": "3.957.0",
1309
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.957.0.tgz",
1310
+ "integrity": "sha512-50vcHu96XakQnIvlKJ1UoltrFODjsq2KvtTgHiPFteUS884lQnK5VC/8xd1Msz/1ONpLMzdCVproCQqhDTtMPQ==",
1311
+ "license": "Apache-2.0",
1312
+ "dependencies": {
1313
+ "@aws-sdk/core": "3.957.0",
1314
+ "@aws-sdk/types": "3.957.0",
1315
+ "@aws-sdk/util-endpoints": "3.957.0",
1316
+ "@smithy/core": "^3.20.0",
1317
+ "@smithy/protocol-http": "^5.3.7",
1318
+ "@smithy/types": "^4.11.0",
1319
+ "tslib": "^2.6.2"
1320
+ },
1321
+ "engines": {
1322
+ "node": ">=18.0.0"
1323
+ }
1324
+ },
1325
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": {
1326
+ "version": "3.958.0",
1327
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.958.0.tgz",
1328
+ "integrity": "sha512-/KuCcS8b5TpQXkYOrPLYytrgxBhv81+5pChkOlhegbeHttjM69pyUpQVJqyfDM/A7wPLnDrzCAnk4zaAOkY0Nw==",
1329
+ "license": "Apache-2.0",
1330
+ "dependencies": {
1331
+ "@aws-crypto/sha256-browser": "5.2.0",
1332
+ "@aws-crypto/sha256-js": "5.2.0",
1333
+ "@aws-sdk/core": "3.957.0",
1334
+ "@aws-sdk/middleware-host-header": "3.957.0",
1335
+ "@aws-sdk/middleware-logger": "3.957.0",
1336
+ "@aws-sdk/middleware-recursion-detection": "3.957.0",
1337
+ "@aws-sdk/middleware-user-agent": "3.957.0",
1338
+ "@aws-sdk/region-config-resolver": "3.957.0",
1339
+ "@aws-sdk/types": "3.957.0",
1340
+ "@aws-sdk/util-endpoints": "3.957.0",
1341
+ "@aws-sdk/util-user-agent-browser": "3.957.0",
1342
+ "@aws-sdk/util-user-agent-node": "3.957.0",
1343
+ "@smithy/config-resolver": "^4.4.5",
1344
+ "@smithy/core": "^3.20.0",
1345
+ "@smithy/fetch-http-handler": "^5.3.8",
1346
+ "@smithy/hash-node": "^4.2.7",
1347
+ "@smithy/invalid-dependency": "^4.2.7",
1348
+ "@smithy/middleware-content-length": "^4.2.7",
1349
+ "@smithy/middleware-endpoint": "^4.4.1",
1350
+ "@smithy/middleware-retry": "^4.4.17",
1351
+ "@smithy/middleware-serde": "^4.2.8",
1352
+ "@smithy/middleware-stack": "^4.2.7",
1353
+ "@smithy/node-config-provider": "^4.3.7",
1354
+ "@smithy/node-http-handler": "^4.4.7",
1355
+ "@smithy/protocol-http": "^5.3.7",
1356
+ "@smithy/smithy-client": "^4.10.2",
1357
+ "@smithy/types": "^4.11.0",
1358
+ "@smithy/url-parser": "^4.2.7",
1359
+ "@smithy/util-base64": "^4.3.0",
1360
+ "@smithy/util-body-length-browser": "^4.2.0",
1361
+ "@smithy/util-body-length-node": "^4.2.1",
1362
+ "@smithy/util-defaults-mode-browser": "^4.3.16",
1363
+ "@smithy/util-defaults-mode-node": "^4.2.19",
1364
+ "@smithy/util-endpoints": "^3.2.7",
1365
+ "@smithy/util-middleware": "^4.2.7",
1366
+ "@smithy/util-retry": "^4.2.7",
1367
+ "@smithy/util-utf8": "^4.2.0",
1368
+ "tslib": "^2.6.2"
1369
+ },
1370
+ "engines": {
1371
+ "node": ">=18.0.0"
1372
+ }
1373
+ },
1374
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/region-config-resolver": {
1375
+ "version": "3.957.0",
1376
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.957.0.tgz",
1377
+ "integrity": "sha512-V8iY3blh8l2iaOqXWW88HbkY5jDoWjH56jonprG/cpyqqCnprvpMUZWPWYJoI8rHRf2bqzZeql1slxG6EnKI7A==",
1378
+ "license": "Apache-2.0",
1379
+ "dependencies": {
1380
+ "@aws-sdk/types": "3.957.0",
1381
+ "@smithy/config-resolver": "^4.4.5",
1382
+ "@smithy/node-config-provider": "^4.3.7",
1383
+ "@smithy/types": "^4.11.0",
1384
+ "tslib": "^2.6.2"
1385
+ },
1386
+ "engines": {
1387
+ "node": ">=18.0.0"
1388
+ }
1389
+ },
1390
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": {
1391
+ "version": "3.957.0",
1392
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.957.0.tgz",
1393
+ "integrity": "sha512-t6UfP1xMUigMMzHcb7vaZcjv7dA2DQkk9C/OAP1dKyrE0vb4lFGDaTApi17GN6Km9zFxJthEMUbBc7DL0hq1Bg==",
1394
+ "license": "Apache-2.0",
1395
+ "dependencies": {
1396
+ "@aws-sdk/middleware-sdk-s3": "3.957.0",
1397
+ "@aws-sdk/types": "3.957.0",
1398
+ "@smithy/protocol-http": "^5.3.7",
1399
+ "@smithy/signature-v4": "^5.3.7",
1400
+ "@smithy/types": "^4.11.0",
1401
+ "tslib": "^2.6.2"
1402
+ },
1403
+ "engines": {
1404
+ "node": ">=18.0.0"
1405
+ }
1406
+ },
1407
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": {
1408
+ "version": "3.958.0",
1409
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.958.0.tgz",
1410
+ "integrity": "sha512-UCj7lQXODduD1myNJQkV+LYcGYJ9iiMggR8ow8Hva1g3A/Na5imNXzz6O67k7DAee0TYpy+gkNw+SizC6min8Q==",
1411
+ "license": "Apache-2.0",
1412
+ "dependencies": {
1413
+ "@aws-sdk/core": "3.957.0",
1414
+ "@aws-sdk/nested-clients": "3.958.0",
1415
+ "@aws-sdk/types": "3.957.0",
1416
+ "@smithy/property-provider": "^4.2.7",
1417
+ "@smithy/shared-ini-file-loader": "^4.4.2",
1418
+ "@smithy/types": "^4.11.0",
1419
+ "tslib": "^2.6.2"
1420
+ },
1421
+ "engines": {
1422
+ "node": ">=18.0.0"
1423
+ }
1424
+ },
1425
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/types": {
1426
+ "version": "3.957.0",
1427
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.957.0.tgz",
1428
+ "integrity": "sha512-wzWC2Nrt859ABk6UCAVY/WYEbAd7FjkdrQL6m24+tfmWYDNRByTJ9uOgU/kw9zqLCAwb//CPvrJdhqjTznWXAg==",
1429
+ "license": "Apache-2.0",
1430
+ "dependencies": {
1431
+ "@smithy/types": "^4.11.0",
1432
+ "tslib": "^2.6.2"
1433
+ },
1434
+ "engines": {
1435
+ "node": ">=18.0.0"
1436
+ }
1437
+ },
1438
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-arn-parser": {
1439
+ "version": "3.957.0",
1440
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.957.0.tgz",
1441
+ "integrity": "sha512-Aj6m+AyrhWyg8YQ4LDPg2/gIfGHCEcoQdBt5DeSFogN5k9mmJPOJ+IAmNSWmWRjpOxEy6eY813RNDI6qS97M0g==",
1442
+ "license": "Apache-2.0",
1443
+ "dependencies": {
1444
+ "tslib": "^2.6.2"
1445
+ },
1446
+ "engines": {
1447
+ "node": ">=18.0.0"
1448
+ }
1449
+ },
1450
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": {
1451
+ "version": "3.957.0",
1452
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.957.0.tgz",
1453
+ "integrity": "sha512-xwF9K24mZSxcxKS3UKQFeX/dPYkEps9wF1b+MGON7EvnbcucrJGyQyK1v1xFPn1aqXkBTFi+SZaMRx5E5YCVFw==",
1454
+ "license": "Apache-2.0",
1455
+ "dependencies": {
1456
+ "@aws-sdk/types": "3.957.0",
1457
+ "@smithy/types": "^4.11.0",
1458
+ "@smithy/url-parser": "^4.2.7",
1459
+ "@smithy/util-endpoints": "^3.2.7",
1460
+ "tslib": "^2.6.2"
1461
+ },
1462
+ "engines": {
1463
+ "node": ">=18.0.0"
1464
+ }
1465
+ },
1466
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": {
1467
+ "version": "3.957.0",
1468
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.957.0.tgz",
1469
+ "integrity": "sha512-exueuwxef0lUJRnGaVkNSC674eAiWU07ORhxBnevFFZEKisln+09Qrtw823iyv5I1N8T+wKfh95xvtWQrNKNQw==",
1470
+ "license": "Apache-2.0",
1471
+ "dependencies": {
1472
+ "@aws-sdk/types": "3.957.0",
1473
+ "@smithy/types": "^4.11.0",
1474
+ "bowser": "^2.11.0",
1475
+ "tslib": "^2.6.2"
1476
+ }
1477
+ },
1478
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-node": {
1479
+ "version": "3.957.0",
1480
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.957.0.tgz",
1481
+ "integrity": "sha512-ycbYCwqXk4gJGp0Oxkzf2KBeeGBdTxz559D41NJP8FlzSej1Gh7Rk40Zo6AyTfsNWkrl/kVi1t937OIzC5t+9Q==",
1482
+ "license": "Apache-2.0",
1483
+ "dependencies": {
1484
+ "@aws-sdk/middleware-user-agent": "3.957.0",
1485
+ "@aws-sdk/types": "3.957.0",
1486
+ "@smithy/node-config-provider": "^4.3.7",
1487
+ "@smithy/types": "^4.11.0",
1488
+ "tslib": "^2.6.2"
1489
+ },
1490
+ "engines": {
1491
+ "node": ">=18.0.0"
1492
+ },
1493
+ "peerDependencies": {
1494
+ "aws-crt": ">=1.0.0"
1495
+ },
1496
+ "peerDependenciesMeta": {
1497
+ "aws-crt": {
1498
+ "optional": true
1499
+ }
1500
+ }
1501
+ },
1502
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/xml-builder": {
1503
+ "version": "3.957.0",
1504
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.957.0.tgz",
1505
+ "integrity": "sha512-Ai5iiQqS8kJ5PjzMhWcLKN0G2yasAkvpnPlq2EnqlIMdB48HsizElt62qcktdxp4neRMyGkFq4NzgmDbXnhRiA==",
1506
+ "license": "Apache-2.0",
1507
+ "dependencies": {
1508
+ "@smithy/types": "^4.11.0",
1509
+ "fast-xml-parser": "5.2.5",
1510
+ "tslib": "^2.6.2"
1511
+ },
1512
+ "engines": {
1513
+ "node": ">=18.0.0"
1514
+ }
1515
+ },
946
1516
"node_modules/@aws-sdk/client-sso": {
947
1517
"version": "3.948.0",
948
1518
"license": "Apache-2.0",
1770
2340
"@babel/core": "^7.0.0-0"
1771
2341
}
1772
2342
},
2343
+ "node_modules/@babel/runtime": {
2344
+ "version": "7.28.4",
2345
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
2346
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
2347
+ "license": "MIT",
2348
+ "engines": {
2349
+ "node": ">=6.9.0"
2350
+ }
2351
+ },
1773
2352
"node_modules/@babel/template": {
1774
2353
"version": "7.27.2",
1775
2354
"license": "MIT",
1813
2392
"version": "1.0.2",
1814
2393
"license": "Apache-2.0"
1815
2394
},
2395
+ "node_modules/@dimforge/rapier3d-compat": {
2396
+ "version": "0.12.0",
2397
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
2398
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
2399
+ "dev": true,
2400
+ "license": "Apache-2.0"
2401
+ },
1816
2402
"node_modules/@drizzle-team/brocli": {
1817
2403
"version": "0.10.2",
1818
2404
"dev": true,
2669
3255
"node": ">=18"
2670
3256
}
2671
3257
},
2672
- "node_modules/@floating-ui/core": {
2673
- "version": "1.7.3",
2674
- "license": "MIT",
2675
- "dependencies": {
2676
- "@floating-ui/utils": "^0.2.10"
2677
- }
2678
- },
2679
- "node_modules/@floating-ui/dom": {
3258
+ "node_modules/@faker-js/faker": {
3259
+ "version": "10.1.0",
3260
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
3261
+ "integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
3262
+ "dev": true,
3263
+ "funding": [
3264
+ {
3265
+ "type": "opencollective",
3266
+ "url": "https://opencollective.com/fakerjs"
3267
+ }
3268
+ ],
3269
+ "license": "MIT",
3270
+ "engines": {
3271
+ "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
3272
+ "npm": ">=10"
3273
+ }
3274
+ },
3275
+ "node_modules/@floating-ui/core": {
3276
+ "version": "1.7.3",
3277
+ "license": "MIT",
3278
+ "dependencies": {
3279
+ "@floating-ui/utils": "^0.2.10"
3280
+ }
3281
+ },
3282
+ "node_modules/@floating-ui/dom": {
2680
3283
"version": "1.7.4",
2681
3284
"license": "MIT",
2682
3285
"dependencies": {
6509
7112
}
6510
7113
},
6511
7114
"node_modules/@smithy/abort-controller": {
6512
- "version": "4.2.5",
7115
+ "version": "4.2.7",
7116
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz",
7117
+ "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==",
6513
7118
"license": "Apache-2.0",
6514
7119
"dependencies": {
6515
- "@smithy/types": "^4.9.0",
7120
+ "@smithy/types": "^4.11.0",
6516
7121
"tslib": "^2.6.2"
6517
7122
},
6518
7123
"engines": {
6541
7146
}
6542
7147
},
6543
7148
"node_modules/@smithy/config-resolver": {
6544
- "version": "4.4.3",
7149
+ "version": "4.4.5",
7150
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz",
7151
+ "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==",
6545
7152
"license": "Apache-2.0",
6546
7153
"dependencies": {
6547
- "@smithy/node-config-provider": "^4.3.5",
6548
- "@smithy/types": "^4.9.0",
7154
+ "@smithy/node-config-provider": "^4.3.7",
7155
+ "@smithy/types": "^4.11.0",
6549
7156
"@smithy/util-config-provider": "^4.2.0",
6550
- "@smithy/util-endpoints": "^3.2.5",
6551
- "@smithy/util-middleware": "^4.2.5",
7157
+ "@smithy/util-endpoints": "^3.2.7",
7158
+ "@smithy/util-middleware": "^4.2.7",
6552
7159
"tslib": "^2.6.2"
6553
7160
},
6554
7161
"engines": {
6556
7163
}
6557
7164
},
6558
7165
"node_modules/@smithy/core": {
6559
- "version": "3.18.7",
7166
+ "version": "3.20.0",
7167
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz",
7168
+ "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==",
6560
7169
"license": "Apache-2.0",
6561
7170
"dependencies": {
6562
- "@smithy/middleware-serde": "^4.2.6",
6563
- "@smithy/protocol-http": "^5.3.5",
6564
- "@smithy/types": "^4.9.0",
7171
+ "@smithy/middleware-serde": "^4.2.8",
7172
+ "@smithy/protocol-http": "^5.3.7",
7173
+ "@smithy/types": "^4.11.0",
6565
7174
"@smithy/util-base64": "^4.3.0",
6566
7175
"@smithy/util-body-length-browser": "^4.2.0",
6567
- "@smithy/util-middleware": "^4.2.5",
6568
- "@smithy/util-stream": "^4.5.6",
7176
+ "@smithy/util-middleware": "^4.2.7",
7177
+ "@smithy/util-stream": "^4.5.8",
6569
7178
"@smithy/util-utf8": "^4.2.0",
6570
7179
"@smithy/uuid": "^1.1.0",
6571
7180
"tslib": "^2.6.2"
6575
7184
}
6576
7185
},
6577
7186
"node_modules/@smithy/credential-provider-imds": {
6578
- "version": "4.2.5",
7187
+ "version": "4.2.7",
7188
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz",
7189
+ "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==",
6579
7190
"license": "Apache-2.0",
6580
7191
"dependencies": {
6581
- "@smithy/node-config-provider": "^4.3.5",
6582
- "@smithy/property-provider": "^4.2.5",
6583
- "@smithy/types": "^4.9.0",
6584
- "@smithy/url-parser": "^4.2.5",
7192
+ "@smithy/node-config-provider": "^4.3.7",
7193
+ "@smithy/property-provider": "^4.2.7",
7194
+ "@smithy/types": "^4.11.0",
7195
+ "@smithy/url-parser": "^4.2.7",
6585
7196
"tslib": "^2.6.2"
6586
7197
},
6587
7198
"engines": {
6649
7260
}
6650
7261
},
6651
7262
"node_modules/@smithy/fetch-http-handler": {
6652
- "version": "5.3.6",
7263
+ "version": "5.3.8",
7264
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz",
7265
+ "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==",
6653
7266
"license": "Apache-2.0",
6654
7267
"dependencies": {
6655
- "@smithy/protocol-http": "^5.3.5",
6656
- "@smithy/querystring-builder": "^4.2.5",
6657
- "@smithy/types": "^4.9.0",
7268
+ "@smithy/protocol-http": "^5.3.7",
7269
+ "@smithy/querystring-builder": "^4.2.7",
7270
+ "@smithy/types": "^4.11.0",
6658
7271
"@smithy/util-base64": "^4.3.0",
6659
7272
"tslib": "^2.6.2"
6660
7273
},
6676
7289
}
6677
7290
},
6678
7291
"node_modules/@smithy/hash-node": {
6679
- "version": "4.2.5",
7292
+ "version": "4.2.7",
7293
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz",
7294
+ "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==",
6680
7295
"license": "Apache-2.0",
6681
7296
"dependencies": {
6682
- "@smithy/types": "^4.9.0",
7297
+ "@smithy/types": "^4.11.0",
6683
7298
"@smithy/util-buffer-from": "^4.2.0",
6684
7299
"@smithy/util-utf8": "^4.2.0",
6685
7300
"tslib": "^2.6.2"
6701
7316
}
6702
7317
},
6703
7318
"node_modules/@smithy/invalid-dependency": {
6704
- "version": "4.2.5",
7319
+ "version": "4.2.7",
7320
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz",
7321
+ "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==",
6705
7322
"license": "Apache-2.0",
6706
7323
"dependencies": {
6707
- "@smithy/types": "^4.9.0",
7324
+ "@smithy/types": "^4.11.0",
6708
7325
"tslib": "^2.6.2"
6709
7326
},
6710
7327
"engines": {
6734
7351
}
6735
7352
},
6736
7353
"node_modules/@smithy/middleware-content-length": {
6737
- "version": "4.2.5",
7354
+ "version": "4.2.7",
7355
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz",
7356
+ "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==",
6738
7357
"license": "Apache-2.0",
6739
7358
"dependencies": {
6740
- "@smithy/protocol-http": "^5.3.5",
6741
- "@smithy/types": "^4.9.0",
7359
+ "@smithy/protocol-http": "^5.3.7",
7360
+ "@smithy/types": "^4.11.0",
6742
7361
"tslib": "^2.6.2"
6743
7362
},
6744
7363
"engines": {
6746
7365
}
6747
7366
},
6748
7367
"node_modules/@smithy/middleware-endpoint": {
6749
- "version": "4.3.14",
7368
+ "version": "4.4.1",
7369
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz",
7370
+ "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==",
6750
7371
"license": "Apache-2.0",
6751
7372
"dependencies": {
6752
- "@smithy/core": "^3.18.7",
6753
- "@smithy/middleware-serde": "^4.2.6",
6754
- "@smithy/node-config-provider": "^4.3.5",
6755
- "@smithy/shared-ini-file-loader": "^4.4.0",
6756
- "@smithy/types": "^4.9.0",
6757
- "@smithy/url-parser": "^4.2.5",
6758
- "@smithy/util-middleware": "^4.2.5",
7373
+ "@smithy/core": "^3.20.0",
7374
+ "@smithy/middleware-serde": "^4.2.8",
7375
+ "@smithy/node-config-provider": "^4.3.7",
7376
+ "@smithy/shared-ini-file-loader": "^4.4.2",
7377
+ "@smithy/types": "^4.11.0",
7378
+ "@smithy/url-parser": "^4.2.7",
7379
+ "@smithy/util-middleware": "^4.2.7",
6759
7380
"tslib": "^2.6.2"
6760
7381
},
6761
7382
"engines": {
6763
7384
}
6764
7385
},
6765
7386
"node_modules/@smithy/middleware-retry": {
6766
- "version": "4.4.14",
7387
+ "version": "4.4.17",
7388
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz",
7389
+ "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==",
6767
7390
"license": "Apache-2.0",
6768
7391
"dependencies": {
6769
- "@smithy/node-config-provider": "^4.3.5",
6770
- "@smithy/protocol-http": "^5.3.5",
6771
- "@smithy/service-error-classification": "^4.2.5",
6772
- "@smithy/smithy-client": "^4.9.10",
6773
- "@smithy/types": "^4.9.0",
6774
- "@smithy/util-middleware": "^4.2.5",
6775
- "@smithy/util-retry": "^4.2.5",
7392
+ "@smithy/node-config-provider": "^4.3.7",
7393
+ "@smithy/protocol-http": "^5.3.7",
7394
+ "@smithy/service-error-classification": "^4.2.7",
7395
+ "@smithy/smithy-client": "^4.10.2",
7396
+ "@smithy/types": "^4.11.0",
7397
+ "@smithy/util-middleware": "^4.2.7",
7398
+ "@smithy/util-retry": "^4.2.7",
6776
7399
"@smithy/uuid": "^1.1.0",
6777
7400
"tslib": "^2.6.2"
6778
7401
},
6781
7404
}
6782
7405
},
6783
7406
"node_modules/@smithy/middleware-serde": {
6784
- "version": "4.2.6",
7407
+ "version": "4.2.8",
7408
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz",
7409
+ "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==",
6785
7410
"license": "Apache-2.0",
6786
7411
"dependencies": {
6787
- "@smithy/protocol-http": "^5.3.5",
6788
- "@smithy/types": "^4.9.0",
7412
+ "@smithy/protocol-http": "^5.3.7",
7413
+ "@smithy/types": "^4.11.0",
6789
7414
"tslib": "^2.6.2"
6790
7415
},
6791
7416
"engines": {
6793
7418
}
6794
7419
},
6795
7420
"node_modules/@smithy/middleware-stack": {
6796
- "version": "4.2.5",
7421
+ "version": "4.2.7",
7422
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz",
7423
+ "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==",
6797
7424
"license": "Apache-2.0",
6798
7425
"dependencies": {
6799
- "@smithy/types": "^4.9.0",
7426
+ "@smithy/types": "^4.11.0",
6800
7427
"tslib": "^2.6.2"
6801
7428
},
6802
7429
"engines": {
6804
7431
}
6805
7432
},
6806
7433
"node_modules/@smithy/node-config-provider": {
6807
- "version": "4.3.5",
7434
+ "version": "4.3.7",
7435
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz",
7436
+ "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==",
6808
7437
"license": "Apache-2.0",
6809
7438
"dependencies": {
6810
- "@smithy/property-provider": "^4.2.5",
6811
- "@smithy/shared-ini-file-loader": "^4.4.0",
6812
- "@smithy/types": "^4.9.0",
7439
+ "@smithy/property-provider": "^4.2.7",
7440
+ "@smithy/shared-ini-file-loader": "^4.4.2",
7441
+ "@smithy/types": "^4.11.0",
6813
7442
"tslib": "^2.6.2"
6814
7443
},
6815
7444
"engines": {
6817
7446
}
6818
7447
},
6819
7448
"node_modules/@smithy/node-http-handler": {
6820
- "version": "4.4.5",
7449
+ "version": "4.4.7",
7450
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz",
7451
+ "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==",
6821
7452
"license": "Apache-2.0",
6822
7453
"dependencies": {
6823
- "@smithy/abort-controller": "^4.2.5",
6824
- "@smithy/protocol-http": "^5.3.5",
6825
- "@smithy/querystring-builder": "^4.2.5",
6826
- "@smithy/types": "^4.9.0",
7454
+ "@smithy/abort-controller": "^4.2.7",
7455
+ "@smithy/protocol-http": "^5.3.7",
7456
+ "@smithy/querystring-builder": "^4.2.7",
7457
+ "@smithy/types": "^4.11.0",
6827
7458
"tslib": "^2.6.2"
6828
7459
},
6829
7460
"engines": {
6831
7462
}
6832
7463
},
6833
7464
"node_modules/@smithy/property-provider": {
6834
- "version": "4.2.5",
7465
+ "version": "4.2.7",
7466
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz",
7467
+ "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==",
6835
7468
"license": "Apache-2.0",
6836
7469
"dependencies": {
6837
- "@smithy/types": "^4.9.0",
7470
+ "@smithy/types": "^4.11.0",
6838
7471
"tslib": "^2.6.2"
6839
7472
},
6840
7473
"engines": {
6842
7475
}
6843
7476
},
6844
7477
"node_modules/@smithy/protocol-http": {
6845
- "version": "5.3.5",
7478
+ "version": "5.3.7",
7479
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz",
7480
+ "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==",
6846
7481
"license": "Apache-2.0",
6847
7482
"dependencies": {
6848
- "@smithy/types": "^4.9.0",
7483
+ "@smithy/types": "^4.11.0",
6849
7484
"tslib": "^2.6.2"
6850
7485
},
6851
7486
"engines": {
6853
7488
}
6854
7489
},
6855
7490
"node_modules/@smithy/querystring-builder": {
6856
- "version": "4.2.5",
7491
+ "version": "4.2.7",
7492
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz",
7493
+ "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==",
6857
7494
"license": "Apache-2.0",
6858
7495
"dependencies": {
6859
- "@smithy/types": "^4.9.0",
7496
+ "@smithy/types": "^4.11.0",
6860
7497
"@smithy/util-uri-escape": "^4.2.0",
6861
7498
"tslib": "^2.6.2"
6862
7499
},
6865
7502
}
6866
7503
},
6867
7504
"node_modules/@smithy/querystring-parser": {
6868
- "version": "4.2.5",
7505
+ "version": "4.2.7",
7506
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz",
7507
+ "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==",
6869
7508
"license": "Apache-2.0",
6870
7509
"dependencies": {
6871
- "@smithy/types": "^4.9.0",
7510
+ "@smithy/types": "^4.11.0",
6872
7511
"tslib": "^2.6.2"
6873
7512
},
6874
7513
"engines": {
6876
7515
}
6877
7516
},
6878
7517
"node_modules/@smithy/service-error-classification": {
6879
- "version": "4.2.5",
7518
+ "version": "4.2.7",
7519
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz",
7520
+ "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==",
6880
7521
"license": "Apache-2.0",
6881
7522
"dependencies": {
6882
- "@smithy/types": "^4.9.0"
7523
+ "@smithy/types": "^4.11.0"
6883
7524
},
6884
7525
"engines": {
6885
7526
"node": ">=18.0.0"
6886
7527
}
6887
7528
},
6888
7529
"node_modules/@smithy/shared-ini-file-loader": {
6889
- "version": "4.4.0",
7530
+ "version": "4.4.2",
7531
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz",
7532
+ "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==",
6890
7533
"license": "Apache-2.0",
6891
7534
"dependencies": {
6892
- "@smithy/types": "^4.9.0",
7535
+ "@smithy/types": "^4.11.0",
6893
7536
"tslib": "^2.6.2"
6894
7537
},
6895
7538
"engines": {
6897
7540
}
6898
7541
},
6899
7542
"node_modules/@smithy/signature-v4": {
6900
- "version": "5.3.5",
7543
+ "version": "5.3.7",
7544
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz",
7545
+ "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==",
6901
7546
"license": "Apache-2.0",
6902
7547
"dependencies": {
6903
7548
"@smithy/is-array-buffer": "^4.2.0",
6904
- "@smithy/protocol-http": "^5.3.5",
6905
- "@smithy/types": "^4.9.0",
7549
+ "@smithy/protocol-http": "^5.3.7",
7550
+ "@smithy/types": "^4.11.0",
6906
7551
"@smithy/util-hex-encoding": "^4.2.0",
6907
- "@smithy/util-middleware": "^4.2.5",
7552
+ "@smithy/util-middleware": "^4.2.7",
6908
7553
"@smithy/util-uri-escape": "^4.2.0",
6909
7554
"@smithy/util-utf8": "^4.2.0",
6910
7555
"tslib": "^2.6.2"
6914
7559
}
6915
7560
},
6916
7561
"node_modules/@smithy/smithy-client": {
6917
- "version": "4.9.10",
7562
+ "version": "4.10.2",
7563
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz",
7564
+ "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==",
6918
7565
"license": "Apache-2.0",
6919
7566
"dependencies": {
6920
- "@smithy/core": "^3.18.7",
6921
- "@smithy/middleware-endpoint": "^4.3.14",
6922
- "@smithy/middleware-stack": "^4.2.5",
6923
- "@smithy/protocol-http": "^5.3.5",
6924
- "@smithy/types": "^4.9.0",
6925
- "@smithy/util-stream": "^4.5.6",
7567
+ "@smithy/core": "^3.20.0",
7568
+ "@smithy/middleware-endpoint": "^4.4.1",
7569
+ "@smithy/middleware-stack": "^4.2.7",
7570
+ "@smithy/protocol-http": "^5.3.7",
7571
+ "@smithy/types": "^4.11.0",
7572
+ "@smithy/util-stream": "^4.5.8",
6926
7573
"tslib": "^2.6.2"
6927
7574
},
6928
7575
"engines": {
6930
7577
}
6931
7578
},
6932
7579
"node_modules/@smithy/types": {
6933
- "version": "4.9.0",
7580
+ "version": "4.11.0",
7581
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz",
7582
+ "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==",
6934
7583
"license": "Apache-2.0",
6935
7584
"dependencies": {
6936
7585
"tslib": "^2.6.2"
6940
7589
}
6941
7590
},
6942
7591
"node_modules/@smithy/url-parser": {
6943
- "version": "4.2.5",
7592
+ "version": "4.2.7",
7593
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz",
7594
+ "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==",
6944
7595
"license": "Apache-2.0",
6945
7596
"dependencies": {
6946
- "@smithy/querystring-parser": "^4.2.5",
6947
- "@smithy/types": "^4.9.0",
7597
+ "@smithy/querystring-parser": "^4.2.7",
7598
+ "@smithy/types": "^4.11.0",
6948
7599
"tslib": "^2.6.2"
6949
7600
},
6950
7601
"engines": {
7005
7656
}
7006
7657
},
7007
7658
"node_modules/@smithy/util-defaults-mode-browser": {
7008
- "version": "4.3.13",
7659
+ "version": "4.3.16",
7660
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz",
7661
+ "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==",
7009
7662
"license": "Apache-2.0",
7010
7663
"dependencies": {
7011
- "@smithy/property-provider": "^4.2.5",
7012
- "@smithy/smithy-client": "^4.9.10",
7013
- "@smithy/types": "^4.9.0",
7664
+ "@smithy/property-provider": "^4.2.7",
7665
+ "@smithy/smithy-client": "^4.10.2",
7666
+ "@smithy/types": "^4.11.0",
7014
7667
"tslib": "^2.6.2"
7015
7668
},
7016
7669
"engines": {
7018
7671
}
7019
7672
},
7020
7673
"node_modules/@smithy/util-defaults-mode-node": {
7021
- "version": "4.2.16",
7674
+ "version": "4.2.19",
7675
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz",
7676
+ "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==",
7022
7677
"license": "Apache-2.0",
7023
7678
"dependencies": {
7024
- "@smithy/config-resolver": "^4.4.3",
7025
- "@smithy/credential-provider-imds": "^4.2.5",
7026
- "@smithy/node-config-provider": "^4.3.5",
7027
- "@smithy/property-provider": "^4.2.5",
7028
- "@smithy/smithy-client": "^4.9.10",
7029
- "@smithy/types": "^4.9.0",
7679
+ "@smithy/config-resolver": "^4.4.5",
7680
+ "@smithy/credential-provider-imds": "^4.2.7",
7681
+ "@smithy/node-config-provider": "^4.3.7",
7682
+ "@smithy/property-provider": "^4.2.7",
7683
+ "@smithy/smithy-client": "^4.10.2",
7684
+ "@smithy/types": "^4.11.0",
7030
7685
"tslib": "^2.6.2"
7031
7686
},
7032
7687
"engines": {
7034
7689
}
7035
7690
},
7036
7691
"node_modules/@smithy/util-endpoints": {
7037
- "version": "3.2.5",
7692
+ "version": "3.2.7",
7693
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz",
7694
+ "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==",
7038
7695
"license": "Apache-2.0",
7039
7696
"dependencies": {
7040
- "@smithy/node-config-provider": "^4.3.5",
7041
- "@smithy/types": "^4.9.0",
7697
+ "@smithy/node-config-provider": "^4.3.7",
7698
+ "@smithy/types": "^4.11.0",
7042
7699
"tslib": "^2.6.2"
7043
7700
},
7044
7701
"engines": {
7056
7713
}
7057
7714
},
7058
7715
"node_modules/@smithy/util-middleware": {
7059
- "version": "4.2.5",
7716
+ "version": "4.2.7",
7717
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz",
7718
+ "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==",
7060
7719
"license": "Apache-2.0",
7061
7720
"dependencies": {
7062
- "@smithy/types": "^4.9.0",
7721
+ "@smithy/types": "^4.11.0",
7063
7722
"tslib": "^2.6.2"
7064
7723
},
7065
7724
"engines": {
7067
7726
}
7068
7727
},
7069
7728
"node_modules/@smithy/util-retry": {
7070
- "version": "4.2.5",
7729
+ "version": "4.2.7",
7730
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz",
7731
+ "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==",
7071
7732
"license": "Apache-2.0",
7072
7733
"dependencies": {
7073
- "@smithy/service-error-classification": "^4.2.5",
7074
- "@smithy/types": "^4.9.0",
7734
+ "@smithy/service-error-classification": "^4.2.7",
7735
+ "@smithy/types": "^4.11.0",
7075
7736
"tslib": "^2.6.2"
7076
7737
},
7077
7738
"engines": {
7079
7740
}
7080
7741
},
7081
7742
"node_modules/@smithy/util-stream": {
7082
- "version": "4.5.6",
7743
+ "version": "4.5.8",
7744
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz",
7745
+ "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==",
7083
7746
"license": "Apache-2.0",
7084
7747
"dependencies": {
7085
- "@smithy/fetch-http-handler": "^5.3.6",
7086
- "@smithy/node-http-handler": "^4.4.5",
7087
- "@smithy/types": "^4.9.0",
7748
+ "@smithy/fetch-http-handler": "^5.3.8",
7749
+ "@smithy/node-http-handler": "^4.4.7",
7750
+ "@smithy/types": "^4.11.0",
7088
7751
"@smithy/util-base64": "^4.3.0",
7089
7752
"@smithy/util-buffer-from": "^4.2.0",
7090
7753
"@smithy/util-hex-encoding": "^4.2.0",
7170
7833
"node": ">=4"
7171
7834
}
7172
7835
},
7836
+ "node_modules/@turf/boolean-point-in-polygon": {
7837
+ "version": "7.3.1",
7838
+ "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.1.tgz",
7839
+ "integrity": "sha512-BUPW63vE43LctwkgannjmEFTX1KFR/18SS7WzFahJWK1ZoP0s1jrfxGX+pi0BH/3Dd9mA71hkGKDDnj1Ndcz0g==",
7840
+ "license": "MIT",
7841
+ "dependencies": {
7842
+ "@turf/helpers": "7.3.1",
7843
+ "@turf/invariant": "7.3.1",
7844
+ "@types/geojson": "^7946.0.10",
7845
+ "point-in-polygon-hao": "^1.1.0",
7846
+ "tslib": "^2.8.1"
7847
+ },
7848
+ "funding": {
7849
+ "url": "https://opencollective.com/turf"
7850
+ }
7851
+ },
7852
+ "node_modules/@turf/helpers": {
7853
+ "version": "7.3.1",
7854
+ "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.1.tgz",
7855
+ "integrity": "sha512-zkL34JVhi5XhsuMEO0MUTIIFEJ8yiW1InMu4hu/oRqamlY4mMoZql0viEmH6Dafh/p+zOl8OYvMJ3Vm3rFshgg==",
7856
+ "license": "MIT",
7857
+ "dependencies": {
7858
+ "@types/geojson": "^7946.0.10",
7859
+ "tslib": "^2.8.1"
7860
+ },
7861
+ "funding": {
7862
+ "url": "https://opencollective.com/turf"
7863
+ }
7864
+ },
7865
+ "node_modules/@turf/invariant": {
7866
+ "version": "7.3.1",
7867
+ "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.3.1.tgz",
7868
+ "integrity": "sha512-IdZJfDjIDCLH+Gu2yLFoSM7H23sdetIo5t4ET1/25X8gi3GE2XSqbZwaGjuZgNh02nisBewLqNiJs2bo+hrqZA==",
7869
+ "license": "MIT",
7870
+ "dependencies": {
7871
+ "@turf/helpers": "7.3.1",
7872
+ "@types/geojson": "^7946.0.10",
7873
+ "tslib": "^2.8.1"
7874
+ },
7875
+ "funding": {
7876
+ "url": "https://opencollective.com/turf"
7877
+ }
7878
+ },
7879
+ "node_modules/@tweenjs/tween.js": {
7880
+ "version": "25.0.0",
7881
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
7882
+ "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
7883
+ "license": "MIT"
7884
+ },
7173
7885
"node_modules/@types/aws-lambda": {
7174
7886
"version": "8.10.159",
7175
7887
"license": "MIT"
7283
7995
"version": "1.0.8",
7284
7996
"license": "MIT"
7285
7997
},
7998
+ "node_modules/@types/geojson": {
7999
+ "version": "7946.0.16",
8000
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
8001
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
8002
+ "license": "MIT"
8003
+ },
7286
8004
"node_modules/@types/hast": {
7287
8005
"version": "3.0.4",
7288
8006
"license": "MIT",
7339
8057
"undici-types": "~6.21.0"
7340
8058
}
7341
8059
},
8060
+ "node_modules/@types/nodemailer": {
8061
+ "version": "7.0.4",
8062
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.4.tgz",
8063
+ "integrity": "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==",
8064
+ "license": "MIT",
8065
+ "dependencies": {
8066
+ "@aws-sdk/client-sesv2": "^3.839.0",
8067
+ "@types/node": "*"
8068
+ }
8069
+ },
7342
8070
"node_modules/@types/oracledb": {
7343
8071
"version": "6.5.2",
7344
8072
"license": "MIT",
7348
8076
},
7349
8077
"node_modules/@types/pg": {
7350
8078
"version": "8.16.0",
8079
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz",
8080
+ "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
7351
8081
"license": "MIT",
7352
8082
"dependencies": {
7353
8083
"@types/node": "*",
7404
8134
"dev": true,
7405
8135
"license": "MIT"
7406
8136
},
8137
+ "node_modules/@types/stats.js": {
8138
+ "version": "0.17.4",
8139
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
8140
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
8141
+ "dev": true,
8142
+ "license": "MIT"
8143
+ },
7407
8144
"node_modules/@types/tedious": {
7408
8145
"version": "4.0.14",
7409
8146
"license": "MIT",
7411
8148
"@types/node": "*"
7412
8149
}
7413
8150
},
8151
+ "node_modules/@types/three": {
8152
+ "version": "0.182.0",
8153
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
8154
+ "integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
8155
+ "dev": true,
8156
+ "license": "MIT",
8157
+ "dependencies": {
8158
+ "@dimforge/rapier3d-compat": "~0.12.0",
8159
+ "@tweenjs/tween.js": "~23.1.3",
8160
+ "@types/stats.js": "*",
8161
+ "@types/webxr": ">=0.5.17",
8162
+ "@webgpu/types": "*",
8163
+ "fflate": "~0.8.2",
8164
+ "meshoptimizer": "~0.22.0"
8165
+ }
8166
+ },
8167
+ "node_modules/@types/three/node_modules/@tweenjs/tween.js": {
8168
+ "version": "23.1.3",
8169
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
8170
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
8171
+ "dev": true,
8172
+ "license": "MIT"
8173
+ },
7414
8174
"node_modules/@types/unist": {
7415
8175
"version": "3.0.3",
7416
8176
"license": "MIT"
7417
8177
},
8178
+ "node_modules/@types/webxr": {
8179
+ "version": "0.5.24",
8180
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
8181
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
8182
+ "dev": true,
8183
+ "license": "MIT"
8184
+ },
7418
8185
"node_modules/@ungap/structured-clone": {
7419
8186
"version": "1.3.0",
7420
8187
"license": "ISC"
7534
8301
"url": "https://opencollective.com/vitest"
7535
8302
}
7536
8303
},
8304
+ "node_modules/@webgpu/types": {
8305
+ "version": "0.1.68",
8306
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.68.tgz",
8307
+ "integrity": "sha512-3ab1B59Ojb6RwjOspYLsTpCzbNB3ZaamIAxBMmvnNkiDoLTZUOBXZ9p5nAYVEkQlDdf6qAZWi1pqj9+ypiqznA==",
8308
+ "dev": true,
8309
+ "license": "BSD-3-Clause"
8310
+ },
7537
8311
"node_modules/abbrev": {
7538
8312
"version": "3.0.1",
7539
8313
"license": "ISC",
7551
8325
"node": ">=6.5"
7552
8326
}
7553
8327
},
8328
+ "node_modules/accessor-fn": {
8329
+ "version": "1.5.3",
8330
+ "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
8331
+ "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
8332
+ "license": "MIT",
8333
+ "engines": {
8334
+ "node": ">=12"
8335
+ }
8336
+ },
7554
8337
"node_modules/acorn": {
7555
8338
"version": "8.15.0",
7556
8339
"license": "MIT",
8644
9427
"ieee754": "^1.2.1"
8645
9428
}
8646
9429
},
9430
+ "node_modules/buffer-equal-constant-time": {
9431
+ "version": "1.0.1",
9432
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
9433
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
9434
+ "license": "BSD-3-Clause"
9435
+ },
8647
9436
"node_modules/buffer-from": {
8648
9437
"version": "1.1.2",
8649
9438
"dev": true,
9038
9827
},
9039
9828
"node_modules/clsx": {
9040
9829
"version": "2.1.1",
9830
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
9831
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
9041
9832
"license": "MIT",
9042
9833
"engines": {
9043
9834
"node": ">=6"
9066
9857
"react-dom": "^18 || ^19 || ^19.0.0-rc"
9067
9858
}
9068
9859
},
9860
+ "node_modules/cobe": {
9861
+ "version": "0.6.5",
9862
+ "resolved": "https://registry.npmjs.org/cobe/-/cobe-0.6.5.tgz",
9863
+ "integrity": "sha512-MA8bu81EFY6JjQpj+FovEuhyJ25khx2Q7Lh+ot/UkCJe5yKyDgzdc6u2lGZIOmsZTXK6Itg1i4lQZIJZbPWnAg==",
9864
+ "license": "MIT",
9865
+ "dependencies": {
9866
+ "phenomenon": "^1.6.0"
9867
+ }
9868
+ },
9069
9869
"node_modules/color": {
9070
9870
"version": "4.2.3",
9071
9871
"license": "MIT",
9218
10018
"version": "3.2.3",
9219
10019
"license": "MIT"
9220
10020
},
10021
+ "node_modules/d3-array": {
10022
+ "version": "3.2.4",
10023
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
10024
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
10025
+ "license": "ISC",
10026
+ "dependencies": {
10027
+ "internmap": "1 - 2"
10028
+ },
10029
+ "engines": {
10030
+ "node": ">=12"
10031
+ }
10032
+ },
10033
+ "node_modules/d3-color": {
10034
+ "version": "3.1.0",
10035
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
10036
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
10037
+ "license": "ISC",
10038
+ "engines": {
10039
+ "node": ">=12"
10040
+ }
10041
+ },
10042
+ "node_modules/d3-delaunay": {
10043
+ "version": "6.0.4",
10044
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
10045
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
10046
+ "license": "ISC",
10047
+ "dependencies": {
10048
+ "delaunator": "5"
10049
+ },
10050
+ "engines": {
10051
+ "node": ">=12"
10052
+ }
10053
+ },
10054
+ "node_modules/d3-format": {
10055
+ "version": "3.1.0",
10056
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
10057
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
10058
+ "license": "ISC",
10059
+ "engines": {
10060
+ "node": ">=12"
10061
+ }
10062
+ },
10063
+ "node_modules/d3-geo": {
10064
+ "version": "3.1.1",
10065
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
10066
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
10067
+ "license": "ISC",
10068
+ "dependencies": {
10069
+ "d3-array": "2.5.0 - 3"
10070
+ },
10071
+ "engines": {
10072
+ "node": ">=12"
10073
+ }
10074
+ },
10075
+ "node_modules/d3-geo-voronoi": {
10076
+ "version": "2.1.0",
10077
+ "resolved": "https://registry.npmjs.org/d3-geo-voronoi/-/d3-geo-voronoi-2.1.0.tgz",
10078
+ "integrity": "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==",
10079
+ "license": "ISC",
10080
+ "dependencies": {
10081
+ "d3-array": "3",
10082
+ "d3-delaunay": "6",
10083
+ "d3-geo": "3",
10084
+ "d3-tricontour": "1"
10085
+ },
10086
+ "engines": {
10087
+ "node": ">=12"
10088
+ }
10089
+ },
10090
+ "node_modules/d3-interpolate": {
10091
+ "version": "3.0.1",
10092
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
10093
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
10094
+ "license": "ISC",
10095
+ "dependencies": {
10096
+ "d3-color": "1 - 3"
10097
+ },
10098
+ "engines": {
10099
+ "node": ">=12"
10100
+ }
10101
+ },
10102
+ "node_modules/d3-octree": {
10103
+ "version": "1.1.0",
10104
+ "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
10105
+ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
10106
+ "license": "MIT"
10107
+ },
10108
+ "node_modules/d3-scale": {
10109
+ "version": "4.0.2",
10110
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
10111
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
10112
+ "license": "ISC",
10113
+ "dependencies": {
10114
+ "d3-array": "2.10.0 - 3",
10115
+ "d3-format": "1 - 3",
10116
+ "d3-interpolate": "1.2.0 - 3",
10117
+ "d3-time": "2.1.1 - 3",
10118
+ "d3-time-format": "2 - 4"
10119
+ },
10120
+ "engines": {
10121
+ "node": ">=12"
10122
+ }
10123
+ },
10124
+ "node_modules/d3-scale-chromatic": {
10125
+ "version": "3.1.0",
10126
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
10127
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
10128
+ "license": "ISC",
10129
+ "dependencies": {
10130
+ "d3-color": "1 - 3",
10131
+ "d3-interpolate": "1 - 3"
10132
+ },
10133
+ "engines": {
10134
+ "node": ">=12"
10135
+ }
10136
+ },
10137
+ "node_modules/d3-selection": {
10138
+ "version": "3.0.0",
10139
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
10140
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
10141
+ "license": "ISC",
10142
+ "engines": {
10143
+ "node": ">=12"
10144
+ }
10145
+ },
10146
+ "node_modules/d3-time": {
10147
+ "version": "3.1.0",
10148
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
10149
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
10150
+ "license": "ISC",
10151
+ "dependencies": {
10152
+ "d3-array": "2 - 3"
10153
+ },
10154
+ "engines": {
10155
+ "node": ">=12"
10156
+ }
10157
+ },
10158
+ "node_modules/d3-time-format": {
10159
+ "version": "4.1.0",
10160
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
10161
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
10162
+ "license": "ISC",
10163
+ "dependencies": {
10164
+ "d3-time": "1 - 3"
10165
+ },
10166
+ "engines": {
10167
+ "node": ">=12"
10168
+ }
10169
+ },
10170
+ "node_modules/d3-tricontour": {
10171
+ "version": "1.1.0",
10172
+ "resolved": "https://registry.npmjs.org/d3-tricontour/-/d3-tricontour-1.1.0.tgz",
10173
+ "integrity": "sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==",
10174
+ "license": "ISC",
10175
+ "dependencies": {
10176
+ "d3-delaunay": "6",
10177
+ "d3-scale": "4"
10178
+ },
10179
+ "engines": {
10180
+ "node": ">=12"
10181
+ }
10182
+ },
10183
+ "node_modules/data-bind-mapper": {
10184
+ "version": "1.0.3",
10185
+ "resolved": "https://registry.npmjs.org/data-bind-mapper/-/data-bind-mapper-1.0.3.tgz",
10186
+ "integrity": "sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==",
10187
+ "license": "MIT",
10188
+ "dependencies": {
10189
+ "accessor-fn": "1"
10190
+ },
10191
+ "engines": {
10192
+ "node": ">=12"
10193
+ }
10194
+ },
10195
+ "node_modules/data-uri-to-buffer": {
10196
+ "version": "4.0.1",
10197
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
10198
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
10199
+ "license": "MIT",
10200
+ "engines": {
10201
+ "node": ">= 12"
10202
+ }
10203
+ },
9221
10204
"node_modules/dateformat": {
9222
10205
"version": "4.6.3",
9223
10206
"dev": true,
9287
10270
"url": "https://github.com/sponsors/ljharb"
9288
10271
}
9289
10272
},
10273
+ "node_modules/delaunator": {
10274
+ "version": "5.0.1",
10275
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
10276
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
10277
+ "license": "ISC",
10278
+ "dependencies": {
10279
+ "robust-predicates": "^3.0.2"
10280
+ }
10281
+ },
9290
10282
"node_modules/denque": {
9291
10283
"version": "2.1.0",
9292
10284
"license": "Apache-2.0",
9579
10571
"node": ">= 0.4"
9580
10572
}
9581
10573
},
10574
+ "node_modules/earcut": {
10575
+ "version": "3.0.2",
10576
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
10577
+ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
10578
+ "license": "ISC"
10579
+ },
9582
10580
"node_modules/eastasianwidth": {
9583
10581
"version": "0.2.0",
9584
10582
"license": "MIT"
9585
10583
},
10584
+ "node_modules/ecdsa-sig-formatter": {
10585
+ "version": "1.0.11",
10586
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
10587
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
10588
+ "license": "Apache-2.0",
10589
+ "dependencies": {
10590
+ "safe-buffer": "^5.0.1"
10591
+ }
10592
+ },
9586
10593
"node_modules/ee-first": {
9587
10594
"version": "1.1.1",
9588
10595
"license": "MIT"
9894
10901
}
9895
10902
}
9896
10903
},
10904
+ "node_modules/fetch-blob": {
10905
+ "version": "3.2.0",
10906
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
10907
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
10908
+ "funding": [
10909
+ {
10910
+ "type": "github",
10911
+ "url": "https://github.com/sponsors/jimmywarting"
10912
+ },
10913
+ {
10914
+ "type": "paypal",
10915
+ "url": "https://paypal.me/jimmywarting"
10916
+ }
10917
+ ],
10918
+ "license": "MIT",
10919
+ "dependencies": {
10920
+ "node-domexception": "^1.0.0",
10921
+ "web-streams-polyfill": "^3.0.3"
10922
+ },
10923
+ "engines": {
10924
+ "node": "^12.20 || >= 14.13"
10925
+ }
10926
+ },
10927
+ "node_modules/fflate": {
10928
+ "version": "0.8.2",
10929
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
10930
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
10931
+ "dev": true,
10932
+ "license": "MIT"
10933
+ },
9897
10934
"node_modules/file-uri-to-path": {
9898
10935
"version": "1.0.0",
9899
10936
"license": "MIT"
9944
10981
"node": ">=8"
9945
10982
}
9946
10983
},
10984
+ "node_modules/float-tooltip": {
10985
+ "version": "1.7.5",
10986
+ "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz",
10987
+ "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==",
10988
+ "license": "MIT",
10989
+ "dependencies": {
10990
+ "d3-selection": "2 - 3",
10991
+ "kapsule": "^1.16",
10992
+ "preact": "10"
10993
+ },
10994
+ "engines": {
10995
+ "node": ">=12"
10996
+ }
10997
+ },
9947
10998
"node_modules/for-each": {
9948
10999
"version": "0.3.5",
9949
11000
"license": "MIT",
9978
11029
"node": ">= 18"
9979
11030
}
9980
11031
},
11032
+ "node_modules/formdata-polyfill": {
11033
+ "version": "4.0.10",
11034
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
11035
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
11036
+ "license": "MIT",
11037
+ "dependencies": {
11038
+ "fetch-blob": "^3.1.2"
11039
+ },
11040
+ "engines": {
11041
+ "node": ">=12.20.0"
11042
+ }
11043
+ },
9981
11044
"node_modules/forwarded-parse": {
9982
11045
"version": "2.1.2",
9983
11046
"license": "MIT"
9993
11056
"url": "https://github.com/sponsors/rawify"
9994
11057
}
9995
11058
},
11059
+ "node_modules/frame-ticker": {
11060
+ "version": "1.0.3",
11061
+ "resolved": "https://registry.npmjs.org/frame-ticker/-/frame-ticker-1.0.3.tgz",
11062
+ "integrity": "sha512-E0X2u2JIvbEMrqEg5+4BpTqaD22OwojJI63K7MdKHdncjtAhGRbCR8nJCr2vwEt9NWBPCPcu70X9smPviEBy8Q==",
11063
+ "license": "MIT",
11064
+ "dependencies": {
11065
+ "simplesignal": "^2.1.6"
11066
+ }
11067
+ },
9996
11068
"node_modules/framer-motion": {
9997
11069
"version": "11.18.2",
11070
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
11071
+ "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
9998
11072
"license": "MIT",
9999
11073
"dependencies": {
10000
11074
"motion-dom": "^11.18.1",
10263
11337
"node": ">=10.13.0"
10264
11338
}
10265
11339
},
11340
+ "node_modules/globe.gl": {
11341
+ "version": "2.45.0",
11342
+ "resolved": "https://registry.npmjs.org/globe.gl/-/globe.gl-2.45.0.tgz",
11343
+ "integrity": "sha512-fjkLHVBrnbESkUgklTd4UbcGLciu4nIl49IIi1hclLjI6MU3ASu6JYmf/K5qwPf7I+tNOauQRr4i5Y28JTtHQg==",
11344
+ "license": "MIT",
11345
+ "dependencies": {
11346
+ "@tweenjs/tween.js": "18 - 25",
11347
+ "accessor-fn": "1",
11348
+ "kapsule": "^1.16",
11349
+ "three": ">=0.154 <1",
11350
+ "three-globe": "^2.45",
11351
+ "three-render-objects": "^1.40"
11352
+ },
11353
+ "engines": {
11354
+ "node": ">=12"
11355
+ }
11356
+ },
11357
+ "node_modules/google-auth-library": {
11358
+ "version": "10.5.0",
11359
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
11360
+ "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
11361
+ "license": "Apache-2.0",
11362
+ "dependencies": {
11363
+ "base64-js": "^1.3.0",
11364
+ "ecdsa-sig-formatter": "^1.0.11",
11365
+ "gaxios": "^7.0.0",
11366
+ "gcp-metadata": "^8.0.0",
11367
+ "google-logging-utils": "^1.0.0",
11368
+ "gtoken": "^8.0.0",
11369
+ "jws": "^4.0.0"
11370
+ },
11371
+ "engines": {
11372
+ "node": ">=18"
11373
+ }
11374
+ },
11375
+ "node_modules/google-auth-library/node_modules/gaxios": {
11376
+ "version": "7.1.3",
11377
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
11378
+ "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
11379
+ "license": "Apache-2.0",
11380
+ "dependencies": {
11381
+ "extend": "^3.0.2",
11382
+ "https-proxy-agent": "^7.0.1",
11383
+ "node-fetch": "^3.3.2",
11384
+ "rimraf": "^5.0.1"
11385
+ },
11386
+ "engines": {
11387
+ "node": ">=18"
11388
+ }
11389
+ },
11390
+ "node_modules/google-auth-library/node_modules/gcp-metadata": {
11391
+ "version": "8.1.2",
11392
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
11393
+ "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
11394
+ "license": "Apache-2.0",
11395
+ "dependencies": {
11396
+ "gaxios": "^7.0.0",
11397
+ "google-logging-utils": "^1.0.0",
11398
+ "json-bigint": "^1.0.0"
11399
+ },
11400
+ "engines": {
11401
+ "node": ">=18"
11402
+ }
11403
+ },
11404
+ "node_modules/google-auth-library/node_modules/google-logging-utils": {
11405
+ "version": "1.1.3",
11406
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
11407
+ "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
11408
+ "license": "Apache-2.0",
11409
+ "engines": {
11410
+ "node": ">=14"
11411
+ }
11412
+ },
11413
+ "node_modules/google-auth-library/node_modules/node-fetch": {
11414
+ "version": "3.3.2",
11415
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
11416
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
11417
+ "license": "MIT",
11418
+ "dependencies": {
11419
+ "data-uri-to-buffer": "^4.0.0",
11420
+ "fetch-blob": "^3.1.4",
11421
+ "formdata-polyfill": "^4.0.10"
11422
+ },
11423
+ "engines": {
11424
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
11425
+ },
11426
+ "funding": {
11427
+ "type": "opencollective",
11428
+ "url": "https://opencollective.com/node-fetch"
11429
+ }
11430
+ },
10266
11431
"node_modules/google-logging-utils": {
10267
11432
"version": "0.0.2",
10268
11433
"license": "Apache-2.0",
10270
11435
"node": ">=14"
10271
11436
}
10272
11437
},
11438
+ "node_modules/googleapis": {
11439
+ "version": "169.0.0",
11440
+ "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-169.0.0.tgz",
11441
+ "integrity": "sha512-IOGMG8tljCZSLvYgdojRu6mB10KEsK0J7X62sXXlQz9koe5BUAW+rqkY3qhQM9wXM6hVL3/Hase7XbxoMyeYiQ==",
11442
+ "license": "Apache-2.0",
11443
+ "dependencies": {
11444
+ "google-auth-library": "^10.2.0",
11445
+ "googleapis-common": "^8.0.0"
11446
+ },
11447
+ "engines": {
11448
+ "node": ">=18"
11449
+ }
11450
+ },
11451
+ "node_modules/googleapis-common": {
11452
+ "version": "8.0.1",
11453
+ "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz",
11454
+ "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==",
11455
+ "license": "Apache-2.0",
11456
+ "dependencies": {
11457
+ "extend": "^3.0.2",
11458
+ "gaxios": "^7.0.0-rc.4",
11459
+ "google-auth-library": "^10.1.0",
11460
+ "qs": "^6.7.0",
11461
+ "url-template": "^2.0.8"
11462
+ },
11463
+ "engines": {
11464
+ "node": ">=18.0.0"
11465
+ }
11466
+ },
11467
+ "node_modules/googleapis-common/node_modules/gaxios": {
11468
+ "version": "7.1.3",
11469
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
11470
+ "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
11471
+ "license": "Apache-2.0",
11472
+ "dependencies": {
11473
+ "extend": "^3.0.2",
11474
+ "https-proxy-agent": "^7.0.1",
11475
+ "node-fetch": "^3.3.2",
11476
+ "rimraf": "^5.0.1"
11477
+ },
11478
+ "engines": {
11479
+ "node": ">=18"
11480
+ }
11481
+ },
11482
+ "node_modules/googleapis-common/node_modules/node-fetch": {
11483
+ "version": "3.3.2",
11484
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
11485
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
11486
+ "license": "MIT",
11487
+ "dependencies": {
11488
+ "data-uri-to-buffer": "^4.0.0",
11489
+ "fetch-blob": "^3.1.4",
11490
+ "formdata-polyfill": "^4.0.10"
11491
+ },
11492
+ "engines": {
11493
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
11494
+ },
11495
+ "funding": {
11496
+ "type": "opencollective",
11497
+ "url": "https://opencollective.com/node-fetch"
11498
+ }
11499
+ },
10273
11500
"node_modules/gopd": {
10274
11501
"version": "1.2.0",
10275
11502
"license": "MIT",
10355
11582
"js-yaml": "bin/js-yaml.js"
10356
11583
}
10357
11584
},
10358
- "node_modules/gray-matter/node_modules/js-yaml/node_modules/argparse": {
10359
- "version": "1.0.10",
10360
- "license": "MIT",
10361
- "dependencies": {
10362
- "sprintf-js": "~1.0.2"
11585
+ "node_modules/gray-matter/node_modules/js-yaml/node_modules/argparse": {
11586
+ "version": "1.0.10",
11587
+ "license": "MIT",
11588
+ "dependencies": {
11589
+ "sprintf-js": "~1.0.2"
11590
+ }
11591
+ },
11592
+ "node_modules/gtoken": {
11593
+ "version": "8.0.0",
11594
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
11595
+ "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
11596
+ "license": "MIT",
11597
+ "dependencies": {
11598
+ "gaxios": "^7.0.0",
11599
+ "jws": "^4.0.0"
11600
+ },
11601
+ "engines": {
11602
+ "node": ">=18"
11603
+ }
11604
+ },
11605
+ "node_modules/gtoken/node_modules/gaxios": {
11606
+ "version": "7.1.3",
11607
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
11608
+ "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
11609
+ "license": "Apache-2.0",
11610
+ "dependencies": {
11611
+ "extend": "^3.0.2",
11612
+ "https-proxy-agent": "^7.0.1",
11613
+ "node-fetch": "^3.3.2",
11614
+ "rimraf": "^5.0.1"
11615
+ },
11616
+ "engines": {
11617
+ "node": ">=18"
11618
+ }
11619
+ },
11620
+ "node_modules/gtoken/node_modules/node-fetch": {
11621
+ "version": "3.3.2",
11622
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
11623
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
11624
+ "license": "MIT",
11625
+ "dependencies": {
11626
+ "data-uri-to-buffer": "^4.0.0",
11627
+ "fetch-blob": "^3.1.4",
11628
+ "formdata-polyfill": "^4.0.10"
11629
+ },
11630
+ "engines": {
11631
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
11632
+ },
11633
+ "funding": {
11634
+ "type": "opencollective",
11635
+ "url": "https://opencollective.com/node-fetch"
11636
+ }
11637
+ },
11638
+ "node_modules/h3-js": {
11639
+ "version": "4.4.0",
11640
+ "resolved": "https://registry.npmjs.org/h3-js/-/h3-js-4.4.0.tgz",
11641
+ "integrity": "sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog==",
11642
+ "license": "Apache-2.0",
11643
+ "engines": {
11644
+ "node": ">=4",
11645
+ "npm": ">=3",
11646
+ "yarn": ">=1.3.0"
10363
11647
}
10364
11648
},
10365
11649
"node_modules/has-property-descriptors": {
10707
11991
"node": ">=0.8.19"
10708
11992
}
10709
11993
},
11994
+ "node_modules/index-array-by": {
11995
+ "version": "1.4.2",
11996
+ "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
11997
+ "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
11998
+ "license": "MIT",
11999
+ "engines": {
12000
+ "node": ">=12"
12001
+ }
12002
+ },
10710
12003
"node_modules/inherits": {
10711
12004
"version": "2.0.4",
10712
12005
"license": "ISC"
10715
12008
"version": "1.3.8",
10716
12009
"license": "ISC"
10717
12010
},
12011
+ "node_modules/internmap": {
12012
+ "version": "2.0.3",
12013
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
12014
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
12015
+ "license": "ISC",
12016
+ "engines": {
12017
+ "node": ">=12"
12018
+ }
12019
+ },
10718
12020
"node_modules/ioredis": {
10719
12021
"version": "5.8.2",
10720
12022
"license": "MIT",
10966
12268
"@pkgjs/parseargs": "^0.11.0"
10967
12269
}
10968
12270
},
12271
+ "node_modules/jerrypick": {
12272
+ "version": "1.1.2",
12273
+ "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz",
12274
+ "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==",
12275
+ "license": "MIT",
12276
+ "engines": {
12277
+ "node": ">=12"
12278
+ }
12279
+ },
10969
12280
"node_modules/jiti": {
10970
12281
"version": "1.21.7",
10971
12282
"license": "MIT",
11037
12348
"graceful-fs": "^4.1.6"
11038
12349
}
11039
12350
},
12351
+ "node_modules/jwa": {
12352
+ "version": "2.0.1",
12353
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
12354
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
12355
+ "license": "MIT",
12356
+ "dependencies": {
12357
+ "buffer-equal-constant-time": "^1.0.1",
12358
+ "ecdsa-sig-formatter": "1.0.11",
12359
+ "safe-buffer": "^5.0.1"
12360
+ }
12361
+ },
12362
+ "node_modules/jws": {
12363
+ "version": "4.0.1",
12364
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
12365
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
12366
+ "license": "MIT",
12367
+ "dependencies": {
12368
+ "jwa": "^2.0.1",
12369
+ "safe-buffer": "^5.0.1"
12370
+ }
12371
+ },
12372
+ "node_modules/kapsule": {
12373
+ "version": "1.16.3",
12374
+ "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz",
12375
+ "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==",
12376
+ "license": "MIT",
12377
+ "dependencies": {
12378
+ "lodash-es": "4"
12379
+ },
12380
+ "engines": {
12381
+ "node": ">=12"
12382
+ }
12383
+ },
11040
12384
"node_modules/keyv": {
11041
12385
"version": "5.5.5",
11042
12386
"license": "MIT",
11117
12461
"version": "4.17.21",
11118
12462
"license": "MIT"
11119
12463
},
12464
+ "node_modules/lodash-es": {
12465
+ "version": "4.17.21",
12466
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
12467
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
12468
+ "license": "MIT"
12469
+ },
11120
12470
"node_modules/lodash.camelcase": {
11121
12471
"version": "4.3.0",
11122
12472
"license": "MIT"
11492
12842
"node": ">= 8"
11493
12843
}
11494
12844
},
12845
+ "node_modules/meshoptimizer": {
12846
+ "version": "0.22.0",
12847
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
12848
+ "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
12849
+ "dev": true,
12850
+ "license": "MIT"
12851
+ },
11495
12852
"node_modules/micromark": {
11496
12853
"version": "4.0.2",
11497
12854
"funding": [
12355
13712
"version": "3.1.1",
12356
13713
"license": "MIT"
12357
13714
},
13715
+ "node_modules/node-domexception": {
13716
+ "version": "1.0.0",
13717
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
13718
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
13719
+ "deprecated": "Use your platform's native DOMException instead",
13720
+ "funding": [
13721
+ {
13722
+ "type": "github",
13723
+ "url": "https://github.com/sponsors/jimmywarting"
13724
+ },
13725
+ {
13726
+ "type": "github",
13727
+ "url": "https://paypal.me/jimmywarting"
13728
+ }
13729
+ ],
13730
+ "license": "MIT",
13731
+ "engines": {
13732
+ "node": ">=10.5.0"
13733
+ }
13734
+ },
12358
13735
"node_modules/node-fetch": {
12359
13736
"version": "2.7.0",
12360
13737
"license": "MIT",
12451
13828
"streamx": "^2.15.0"
12452
13829
}
12453
13830
},
13831
+ "node_modules/nodemailer": {
13832
+ "version": "7.0.12",
13833
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz",
13834
+ "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==",
13835
+ "license": "MIT-0",
13836
+ "engines": {
13837
+ "node": ">=6.0.0"
13838
+ }
13839
+ },
12454
13840
"node_modules/nopt": {
12455
13841
"version": "8.1.0",
12456
13842
"license": "ISC",
12502
13888
"node": ">= 6"
12503
13889
}
12504
13890
},
13891
+ "node_modules/object-inspect": {
13892
+ "version": "1.13.4",
13893
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
13894
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
13895
+ "license": "MIT",
13896
+ "engines": {
13897
+ "node": ">= 0.4"
13898
+ },
13899
+ "funding": {
13900
+ "url": "https://github.com/sponsors/ljharb"
13901
+ }
13902
+ },
12505
13903
"node_modules/obug": {
12506
13904
"version": "2.1.1",
12507
13905
"dev": true,
12557
13955
"regex-recursion": "^5.1.1"
12558
13956
}
12559
13957
},
13958
+ "node_modules/openpgp": {
13959
+ "version": "6.3.0",
13960
+ "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-6.3.0.tgz",
13961
+ "integrity": "sha512-pLzCU8IgyKXPSO11eeharQkQ4GzOKNWhXq79pQarIRZEMt1/ssyr+MIuWBv1mNoenJLg04gvPx+fi4gcKZ4bag==",
13962
+ "license": "LGPL-3.0+",
13963
+ "engines": {
13964
+ "node": ">= 18.0.0"
13965
+ }
13966
+ },
12560
13967
"node_modules/ora": {
12561
13968
"version": "8.2.0",
12562
13969
"license": "MIT",
12748
14155
},
12749
14156
"node_modules/pg": {
12750
14157
"version": "8.16.3",
14158
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
14159
+ "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
12751
14160
"license": "MIT",
12752
14161
"peer": true,
12753
14162
"dependencies": {
12820
14229
"split2": "^4.1.0"
12821
14230
}
12822
14231
},
14232
+ "node_modules/phenomenon": {
14233
+ "version": "1.6.0",
14234
+ "resolved": "https://registry.npmjs.org/phenomenon/-/phenomenon-1.6.0.tgz",
14235
+ "integrity": "sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==",
14236
+ "license": "MIT"
14237
+ },
12823
14238
"node_modules/picocolors": {
12824
14239
"version": "1.1.1",
12825
14240
"license": "ISC"
12964
14379
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
12965
14380
}
12966
14381
},
14382
+ "node_modules/point-in-polygon-hao": {
14383
+ "version": "1.2.4",
14384
+ "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz",
14385
+ "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==",
14386
+ "license": "MIT",
14387
+ "dependencies": {
14388
+ "robust-predicates": "^3.0.2"
14389
+ }
14390
+ },
14391
+ "node_modules/polished": {
14392
+ "version": "4.3.1",
14393
+ "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz",
14394
+ "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==",
14395
+ "license": "MIT",
14396
+ "dependencies": {
14397
+ "@babel/runtime": "^7.17.8"
14398
+ },
14399
+ "engines": {
14400
+ "node": ">=10"
14401
+ }
14402
+ },
12967
14403
"node_modules/possible-typed-array-names": {
12968
14404
"version": "1.1.0",
12969
14405
"license": "MIT",
13173
14609
"node": ">=0.10.0"
13174
14610
}
13175
14611
},
14612
+ "node_modules/preact": {
14613
+ "version": "10.28.0",
14614
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.0.tgz",
14615
+ "integrity": "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA==",
14616
+ "license": "MIT",
14617
+ "funding": {
14618
+ "type": "opencollective",
14619
+ "url": "https://opencollective.com/preact"
14620
+ }
14621
+ },
13176
14622
"node_modules/prebuild-install": {
13177
14623
"version": "7.1.3",
13178
14624
"license": "MIT",
13284
14730
"node": ">=6"
13285
14731
}
13286
14732
},
14733
+ "node_modules/prop-types": {
14734
+ "version": "15.8.1",
14735
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
14736
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
14737
+ "license": "MIT",
14738
+ "dependencies": {
14739
+ "loose-envify": "^1.4.0",
14740
+ "object-assign": "^4.1.1",
14741
+ "react-is": "^16.13.1"
14742
+ }
14743
+ },
13287
14744
"node_modules/property-information": {
13288
14745
"version": "7.1.0",
13289
14746
"license": "MIT",
13322
14779
"once": "^1.3.1"
13323
14780
}
13324
14781
},
14782
+ "node_modules/qs": {
14783
+ "version": "6.14.1",
14784
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
14785
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
14786
+ "license": "BSD-3-Clause",
14787
+ "dependencies": {
14788
+ "side-channel": "^1.1.0"
14789
+ },
14790
+ "engines": {
14791
+ "node": ">=0.6"
14792
+ },
14793
+ "funding": {
14794
+ "url": "https://github.com/sponsors/ljharb"
14795
+ }
14796
+ },
13325
14797
"node_modules/queue-microtask": {
13326
14798
"version": "1.2.3",
13327
14799
"funding": [
13404
14876
"react": "^18.3.1"
13405
14877
}
13406
14878
},
14879
+ "node_modules/react-globe.gl": {
14880
+ "version": "2.37.0",
14881
+ "resolved": "https://registry.npmjs.org/react-globe.gl/-/react-globe.gl-2.37.0.tgz",
14882
+ "integrity": "sha512-nN1FDOJBhFvWfKrOY0SnkDuA8wk9FSTBN0HFfAdTqqcFM5R+OXBIxK0BM6t8n3oNVYpEJVxEzjYFwLyk4BC7Cw==",
14883
+ "license": "MIT",
14884
+ "dependencies": {
14885
+ "globe.gl": "^2.45",
14886
+ "prop-types": "15",
14887
+ "react-kapsule": "^2.5"
14888
+ },
14889
+ "engines": {
14890
+ "node": ">=12"
14891
+ },
14892
+ "peerDependencies": {
14893
+ "react": "*"
14894
+ }
14895
+ },
14896
+ "node_modules/react-is": {
14897
+ "version": "16.13.1",
14898
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
14899
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
14900
+ "license": "MIT"
14901
+ },
14902
+ "node_modules/react-kapsule": {
14903
+ "version": "2.5.7",
14904
+ "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz",
14905
+ "integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==",
14906
+ "license": "MIT",
14907
+ "dependencies": {
14908
+ "jerrypick": "^1.1.1"
14909
+ },
14910
+ "engines": {
14911
+ "node": ">=12"
14912
+ },
14913
+ "peerDependencies": {
14914
+ "react": ">=16.13.1"
14915
+ }
14916
+ },
13407
14917
"node_modules/react-refresh": {
13408
14918
"version": "0.17.0",
13409
14919
"license": "MIT",
13844
15354
"node": ">=0.10.0"
13845
15355
}
13846
15356
},
15357
+ "node_modules/rimraf": {
15358
+ "version": "5.0.10",
15359
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
15360
+ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
15361
+ "license": "ISC",
15362
+ "dependencies": {
15363
+ "glob": "^10.3.7"
15364
+ },
15365
+ "bin": {
15366
+ "rimraf": "dist/esm/bin.mjs"
15367
+ },
15368
+ "funding": {
15369
+ "url": "https://github.com/sponsors/isaacs"
15370
+ }
15371
+ },
15372
+ "node_modules/robust-predicates": {
15373
+ "version": "3.0.2",
15374
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
15375
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
15376
+ "license": "Unlicense"
15377
+ },
13847
15378
"node_modules/rollup": {
13848
15379
"version": "4.53.3",
13849
15380
"license": "MIT",
14122
15653
"@types/hast": "^3.0.4"
14123
15654
}
14124
15655
},
15656
+ "node_modules/side-channel": {
15657
+ "version": "1.1.0",
15658
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
15659
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
15660
+ "license": "MIT",
15661
+ "dependencies": {
15662
+ "es-errors": "^1.3.0",
15663
+ "object-inspect": "^1.13.3",
15664
+ "side-channel-list": "^1.0.0",
15665
+ "side-channel-map": "^1.0.1",
15666
+ "side-channel-weakmap": "^1.0.2"
15667
+ },
15668
+ "engines": {
15669
+ "node": ">= 0.4"
15670
+ },
15671
+ "funding": {
15672
+ "url": "https://github.com/sponsors/ljharb"
15673
+ }
15674
+ },
15675
+ "node_modules/side-channel-list": {
15676
+ "version": "1.0.0",
15677
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
15678
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
15679
+ "license": "MIT",
15680
+ "dependencies": {
15681
+ "es-errors": "^1.3.0",
15682
+ "object-inspect": "^1.13.3"
15683
+ },
15684
+ "engines": {
15685
+ "node": ">= 0.4"
15686
+ },
15687
+ "funding": {
15688
+ "url": "https://github.com/sponsors/ljharb"
15689
+ }
15690
+ },
15691
+ "node_modules/side-channel-map": {
15692
+ "version": "1.0.1",
15693
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
15694
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
15695
+ "license": "MIT",
15696
+ "dependencies": {
15697
+ "call-bound": "^1.0.2",
15698
+ "es-errors": "^1.3.0",
15699
+ "get-intrinsic": "^1.2.5",
15700
+ "object-inspect": "^1.13.3"
15701
+ },
15702
+ "engines": {
15703
+ "node": ">= 0.4"
15704
+ },
15705
+ "funding": {
15706
+ "url": "https://github.com/sponsors/ljharb"
15707
+ }
15708
+ },
15709
+ "node_modules/side-channel-weakmap": {
15710
+ "version": "1.0.2",
15711
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
15712
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
15713
+ "license": "MIT",
15714
+ "dependencies": {
15715
+ "call-bound": "^1.0.2",
15716
+ "es-errors": "^1.3.0",
15717
+ "get-intrinsic": "^1.2.5",
15718
+ "object-inspect": "^1.13.3",
15719
+ "side-channel-map": "^1.0.1"
15720
+ },
15721
+ "engines": {
15722
+ "node": ">= 0.4"
15723
+ },
15724
+ "funding": {
15725
+ "url": "https://github.com/sponsors/ljharb"
15726
+ }
15727
+ },
14125
15728
"node_modules/siginfo": {
14126
15729
"version": "2.0.0",
14127
15730
"dev": true,
14199
15802
"is-arrayish": "^0.3.1"
14200
15803
}
14201
15804
},
15805
+ "node_modules/simplesignal": {
15806
+ "version": "2.1.7",
15807
+ "resolved": "https://registry.npmjs.org/simplesignal/-/simplesignal-2.1.7.tgz",
15808
+ "integrity": "sha512-PEo2qWpUke7IMhlqiBxrulIFvhJRLkl1ih52Rwa+bPjzhJepcd4GIjn2RiQmFSx3dQvsEAgF0/lXMwMN7vODaA==",
15809
+ "license": "MIT"
15810
+ },
14202
15811
"node_modules/sisteransi": {
14203
15812
"version": "1.0.5",
14204
15813
"license": "MIT"
14531
16140
},
14532
16141
"node_modules/tailwind-merge": {
14533
16142
"version": "2.6.0",
16143
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
16144
+ "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
14534
16145
"license": "MIT",
14535
16146
"funding": {
14536
16147
"type": "github",
14681
16292
"real-require": "^0.2.0"
14682
16293
}
14683
16294
},
16295
+ "node_modules/three": {
16296
+ "version": "0.182.0",
16297
+ "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
16298
+ "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
16299
+ "license": "MIT",
16300
+ "peer": true
16301
+ },
16302
+ "node_modules/three-conic-polygon-geometry": {
16303
+ "version": "2.1.2",
16304
+ "resolved": "https://registry.npmjs.org/three-conic-polygon-geometry/-/three-conic-polygon-geometry-2.1.2.tgz",
16305
+ "integrity": "sha512-NaP3RWLJIyPGI+zyaZwd0Yj6rkoxm4FJHqAX1Enb4L64oNYLCn4bz1ESgOEYavgcUwCNYINu1AgEoUBJr1wZcA==",
16306
+ "license": "MIT",
16307
+ "dependencies": {
16308
+ "@turf/boolean-point-in-polygon": "^7.2",
16309
+ "d3-array": "1 - 3",
16310
+ "d3-geo": "1 - 3",
16311
+ "d3-geo-voronoi": "2",
16312
+ "d3-scale": "1 - 4",
16313
+ "delaunator": "5",
16314
+ "earcut": "3"
16315
+ },
16316
+ "engines": {
16317
+ "node": ">=12"
16318
+ },
16319
+ "peerDependencies": {
16320
+ "three": ">=0.72.0"
16321
+ }
16322
+ },
16323
+ "node_modules/three-geojson-geometry": {
16324
+ "version": "2.1.1",
16325
+ "resolved": "https://registry.npmjs.org/three-geojson-geometry/-/three-geojson-geometry-2.1.1.tgz",
16326
+ "integrity": "sha512-dC7bF3ri1goDcihYhzACHOBQqu7YNNazYLa2bSydVIiJUb3jDFojKSy+gNj2pMkqZNSVjssSmdY9zlmnhEpr1w==",
16327
+ "license": "MIT",
16328
+ "dependencies": {
16329
+ "d3-geo": "1 - 3",
16330
+ "d3-interpolate": "1 - 3",
16331
+ "earcut": "3"
16332
+ },
16333
+ "engines": {
16334
+ "node": ">=12"
16335
+ },
16336
+ "peerDependencies": {
16337
+ "three": ">=0.72.0"
16338
+ }
16339
+ },
16340
+ "node_modules/three-globe": {
16341
+ "version": "2.45.0",
16342
+ "resolved": "https://registry.npmjs.org/three-globe/-/three-globe-2.45.0.tgz",
16343
+ "integrity": "sha512-Ur6BVkezvmHnvsEg8fbq6gIscSZtknSQMWwDRbiJ95o6OSDjDbGTc4oO6nP7mOM9aAA3YrF7YZyOwSkP4T56QA==",
16344
+ "license": "MIT",
16345
+ "dependencies": {
16346
+ "@tweenjs/tween.js": "18 - 25",
16347
+ "accessor-fn": "1",
16348
+ "d3-array": "3",
16349
+ "d3-color": "3",
16350
+ "d3-geo": "3",
16351
+ "d3-interpolate": "3",
16352
+ "d3-scale": "4",
16353
+ "d3-scale-chromatic": "3",
16354
+ "data-bind-mapper": "1",
16355
+ "frame-ticker": "1",
16356
+ "h3-js": "4",
16357
+ "index-array-by": "1",
16358
+ "kapsule": "^1.16",
16359
+ "three-conic-polygon-geometry": "2",
16360
+ "three-geojson-geometry": "2",
16361
+ "three-slippy-map-globe": "1",
16362
+ "tinycolor2": "1"
16363
+ },
16364
+ "engines": {
16365
+ "node": ">=12"
16366
+ },
16367
+ "peerDependencies": {
16368
+ "three": ">=0.154"
16369
+ }
16370
+ },
16371
+ "node_modules/three-render-objects": {
16372
+ "version": "1.40.4",
16373
+ "resolved": "https://registry.npmjs.org/three-render-objects/-/three-render-objects-1.40.4.tgz",
16374
+ "integrity": "sha512-Ukpu1pei3L5r809izvjsZxwuRcYLiyn6Uvy3lZ9bpMTdvj3i6PeX6w++/hs2ZS3KnEzGjb6YvTvh4UQuwHTDJg==",
16375
+ "license": "MIT",
16376
+ "dependencies": {
16377
+ "@tweenjs/tween.js": "18 - 25",
16378
+ "accessor-fn": "1",
16379
+ "float-tooltip": "^1.7",
16380
+ "kapsule": "^1.16",
16381
+ "polished": "4"
16382
+ },
16383
+ "engines": {
16384
+ "node": ">=12"
16385
+ },
16386
+ "peerDependencies": {
16387
+ "three": ">=0.168"
16388
+ }
16389
+ },
16390
+ "node_modules/three-slippy-map-globe": {
16391
+ "version": "1.0.4",
16392
+ "resolved": "https://registry.npmjs.org/three-slippy-map-globe/-/three-slippy-map-globe-1.0.4.tgz",
16393
+ "integrity": "sha512-am8A4PP38AfTdrhXBDucwPRHLTbBl93yhpjIs56K1TLs9VuUWzg68oim4Dibs9QC1riXbj5SoBp/okA1VN9eYg==",
16394
+ "license": "MIT",
16395
+ "dependencies": {
16396
+ "d3-geo": "1 - 3",
16397
+ "d3-octree": "^1.1",
16398
+ "d3-scale": "1 - 4"
16399
+ },
16400
+ "engines": {
16401
+ "node": ">=12"
16402
+ },
16403
+ "peerDependencies": {
16404
+ "three": ">=0.154"
16405
+ }
16406
+ },
14684
16407
"node_modules/tinybench": {
14685
16408
"version": "2.9.0",
14686
16409
"dev": true,
14687
16410
"license": "MIT"
14688
16411
},
16412
+ "node_modules/tinycolor2": {
16413
+ "version": "1.6.0",
16414
+ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
16415
+ "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
16416
+ "license": "MIT"
16417
+ },
14689
16418
"node_modules/tinyexec": {
14690
16419
"version": "1.0.2",
14691
16420
"dev": true,
15034
16763
"browserslist": ">= 4.21.0"
15035
16764
}
15036
16765
},
16766
+ "node_modules/url-template": {
16767
+ "version": "2.0.8",
16768
+ "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
16769
+ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
16770
+ "license": "BSD"
16771
+ },
15037
16772
"node_modules/use-callback-ref": {
15038
16773
"version": "1.3.3",
15039
16774
"license": "MIT",
15358
17093
"url": "https://github.com/sponsors/wooorm"
15359
17094
}
15360
17095
},
17096
+ "node_modules/web-streams-polyfill": {
17097
+ "version": "3.3.3",
17098
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
17099
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
17100
+ "license": "MIT",
17101
+ "engines": {
17102
+ "node": ">= 8"
17103
+ }
17104
+ },
15361
17105
"node_modules/webidl-conversions": {
15362
17106
"version": "3.0.1",
15363
17107
"license": "BSD-2-Clause"
15364
17108
▼package.json
+13-3
14
14
"db:push": "drizzle-kit push",
15
15
"db:studio": "drizzle-kit studio",
16
16
"git:init": "node scripts/init-git-server.js",
17
+ "git:start": "tsx scripts/ssh-server.ts",
17
18
"runner:start": "node src/runner/index.js"
18
19
},
19
20
"dependencies": {
51
52
"@radix-ui/react-toggle": "^1.1.0",
52
53
"@radix-ui/react-toggle-group": "^1.1.0",
53
54
"@radix-ui/react-tooltip": "^1.1.2",
55
+ "@types/nodemailer": "^7.0.4",
54
56
"astro": "^4.11.0",
55
57
"bcryptjs": "^2.4.3",
56
58
"better-sqlite3": "^11.1.2",
58
60
"class-variance-authority": "^0.7.0",
59
61
"clsx": "^2.1.1",
60
62
"cmdk": "^1.1.1",
63
+ "cobe": "^0.6.5",
61
64
"dockerode": "^4.0.2",
62
65
"drizzle-orm": "^0.45.1",
63
- "framer-motion": "^11.2.12",
66
+ "framer-motion": "^11.18.2",
64
67
"github-markdown-css": "^5.8.1",
68
+ "googleapis": "^169.0.0",
65
69
"ioredis": "^5.8.2",
66
70
"isomorphic-git": "^1.25.10",
67
71
"jose": "^5.6.3",
72
76
"mysql2": "^3.15.3",
73
77
"nanoid": "^5.0.7",
74
78
"nodegit": "^0.28.0-alpha.27",
79
+ "nodemailer": "^7.0.12",
80
+ "openpgp": "^6.3.0",
75
81
"otplib": "^12.0.1",
76
82
"pg": "^8.16.3",
77
83
"pino": "^10.1.0",
79
85
"prom-client": "^15.1.3",
80
86
"react": "^18.3.1",
81
87
"react-dom": "^18.3.1",
88
+ "react-globe.gl": "^2.37.0",
82
89
"rehype-sanitize": "^6.0.0",
83
90
"rehype-stringify": "^10.0.1",
84
91
"remark-gfm": "^4.0.1",
86
93
"remark-rehype": "^11.1.2",
87
94
"simple-git": "^3.25.0",
88
95
"ssh2": "^1.15.0",
89
- "tailwind-merge": "^2.3.0",
96
+ "tailwind-merge": "^2.6.0",
90
97
"tailwindcss-animate": "^1.0.7",
98
+ "three": "^0.182.0",
91
99
"unified": "^11.0.5",
92
100
"zod": "^3.23.8",
93
101
"zustand": "^4.5.4"
94
102
},
95
103
"devDependencies": {
104
+ "@faker-js/faker": "^10.1.0",
96
105
"@playwright/test": "^1.57.0",
97
106
"@tailwindcss/typography": "^0.5.19",
98
107
"@types/bcryptjs": "^2.4.6",
100
109
"@types/dockerode": "^3.3.29",
101
110
"@types/js-yaml": "^4.0.9",
102
111
"@types/node": "^20.14.9",
103
- "@types/pg": "^8.11.6",
112
+ "@types/pg": "^8.16.0",
104
113
"@types/react": "^18.3.3",
105
114
"@types/react-dom": "^18.3.0",
106
115
"@types/ssh2": "^1.15.0",
116
+ "@types/three": "^0.182.0",
107
117
"autoprefixer": "^10.4.19",
108
118
"drizzle-kit": "^0.31.8",
109
119
"pino-pretty": "^13.1.3",
110
120
▼pci/1.png
+0-0
Binary file not shown.
▼pci/image copy 2.png
+0-0
Binary file not shown.
▼pci/image copy 3.png
+0-0
Binary file not shown.
▼pci/image copy 4.png
+0-0
Binary file not shown.
▼pci/image copy 5.png
+0-0
Binary file not shown.
▼pci/image copy.png
+0-0
Binary file not shown.
▼pci/image.png
+0-0
Binary file not shown.
▼scripts/seed-admin.ts
+77-0
1
+import { getDatabase, schema } from "../src/db";
2
+import { eq } from "drizzle-orm";
3
+import readline from "readline";
4
+import bcrypt from "bcryptjs";
5
+
6
+// Utility to prompt input
7
+const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout
10
+});
11
+
12
+const ask = (query: string): Promise<string> =>
13
+ new Promise(resolve => rl.question(query, resolve));
14
+
15
+async function main() {
16
+ console.log("🚀 OpenCodeHub Admin Seeder");
17
+ console.log("---------------------------");
18
+
19
+ const username = process.argv[2] || await ask("Enter username: ");
20
+ if (!username) {
21
+ console.error("Username required");
22
+ process.exit(1);
23
+ }
24
+
25
+ const email = process.argv[3] || await ask("Enter email (if creating new): ");
26
+ const password = process.argv[4] || await ask("Enter password (if creating new): ");
27
+
28
+ const db = getDatabase();
29
+
30
+ // Check if user exists
31
+ const user = await db.query.users.findFirst({
32
+ where: eq(schema.users.username, username)
33
+ });
34
+
35
+ if (user) {
36
+ console.log(`\nUser '${username}' found.`);
37
+ if (user.isAdmin) {
38
+ console.log("✅ User is already an Admin.");
39
+ } else {
40
+ console.log("Promoting to Admin...");
41
+ await db.update(schema.users)
42
+ .set({ isAdmin: true })
43
+ .where(eq(schema.users.id, user.id));
44
+ console.log("✅ User promoted to Admin successfully.");
45
+ }
46
+ } else {
47
+ console.log(`\nUser '${username}' not found. Creating new admin user...`);
48
+ if (!email || !password) {
49
+ console.error("Error: Email and Password are required to create a new user.");
50
+ process.exit(1);
51
+ }
52
+
53
+ const hashedPassword = await bcrypt.hash(password, 10);
54
+ const userId = crypto.randomUUID();
55
+
56
+ await db.insert(schema.users).values({
57
+ id: userId,
58
+ username,
59
+ email,
60
+ passwordHash: hashedPassword,
61
+ displayName: username, // Default display name
62
+ isAdmin: true,
63
+ isActive: true, // Auto-activate admin
64
+ emailVerified: true // Auto-verify admin
65
+ });
66
+
67
+ console.log(`✅ Admin user '${username}' created successfully.`);
68
+ }
69
+
70
+ rl.close();
71
+ process.exit(0);
72
+}
73
+
74
+main().catch(err => {
75
+ console.error("Error:", err);
76
+ process.exit(1);
77
+});
0
78
▼scripts/ssh-server.ts
+119-0
1
+
2
+import { getDatabase, schema } from "@/db";
3
+import { canReadRepo, canWriteRepo } from "@/lib/permissions";
4
+import { startSSHServer } from "@/lib/ssh";
5
+import { and, eq } from "drizzle-orm";
6
+import { join } from "path";
7
+
8
+const DATA_DIR = join(process.cwd(), "data");
9
+const REPOS_DIR = join(DATA_DIR, "repos");
10
+const SSH_DIR = join(DATA_DIR, "ssh");
11
+const HOST_KEY_PATH = join(SSH_DIR, "ssh_host_rsa_key");
12
+
13
+async function main() {
14
+ console.log("Starting OpenCodeHub SSH Server...");
15
+
16
+ await startSSHServer({
17
+ port: 2222,
18
+ hostKeyPath: HOST_KEY_PATH,
19
+ reposPath: REPOS_DIR,
20
+ authenticateUser: async (username, key) => {
21
+ // We ignore the username (usually 'git') and authenticate by key
22
+ const db = getDatabase();
23
+
24
+ // Iterate over all keys to find a match
25
+ // In a production app, we would index by fingerprint or key type to optimize
26
+ const allKeys = await db.query.sshKeys.findMany();
27
+
28
+ for (const dbKey of allKeys) {
29
+ try {
30
+ // Parse DB key
31
+ const parts = dbKey.publicKey.trim().split(" ");
32
+ if (parts.length < 2) continue;
33
+
34
+ const dbKeyData = Buffer.from(parts[1], 'base64');
35
+
36
+ // Compare buffers
37
+ if (dbKeyData.equals(key)) {
38
+ return {
39
+ valid: true,
40
+ userId: dbKey.userId,
41
+ canRead: true, // Specific repo auth is handled in authorizeRepo
42
+ canWrite: true
43
+ };
44
+ }
45
+ } catch (e) {
46
+ console.error("Error checking key:", e);
47
+ }
48
+ }
49
+
50
+ return { valid: false };
51
+ },
52
+ authorizeRepo: async (userId, repoPath, operation) => {
53
+ // repoPath is "owner/repo.git"
54
+ const [ownerName, repoNameWithGit] = repoPath.split("/");
55
+ const repoName = repoNameWithGit.replace(".git", "");
56
+
57
+ if (!ownerName || !repoName) return false;
58
+
59
+ const db = getDatabase();
60
+
61
+ // Find Repo
62
+ const owner = await db.query.users.findFirst({
63
+ where: eq(schema.users.username, ownerName)
64
+ });
65
+
66
+ if (!owner) return false;
67
+
68
+ const repo = await db.query.repositories.findFirst({
69
+ where: and(
70
+ eq(schema.repositories.ownerId, owner.id),
71
+ eq(schema.repositories.name, repoName)
72
+ )
73
+ });
74
+
75
+ if (!repo) return false;
76
+
77
+ if (operation === "read") {
78
+ return canReadRepo(userId, repo);
79
+ } else {
80
+ return canWriteRepo(userId, repo);
81
+ }
82
+ },
83
+ onPush: async (userId, repoPath, refs) => {
84
+ console.log(`Push received to ${repoPath} from ${userId}`);
85
+
86
+ // Resolve Repo ID
87
+ const [ownerName, repoNameWithGit] = repoPath.split("/");
88
+ const repoName = repoNameWithGit.replace(".git", "");
89
+
90
+ const db = getDatabase();
91
+ const owner = await db.query.users.findFirst({ where: eq(schema.users.username, ownerName) });
92
+ if (!owner) return;
93
+
94
+ const repo = await db.query.repositories.findFirst({
95
+ where: and(eq(schema.repositories.ownerId, owner.id), eq(schema.repositories.name, repoName))
96
+ });
97
+
98
+ if (repo) {
99
+ // 1. Run Analysis
100
+ import("@/lib/analysis").then(({ analyzeRepository }) => {
101
+ analyzeRepository(repo.id, userId).catch(console.error);
102
+ });
103
+
104
+ // 2. Trigger Workflows
105
+ import("@/lib/workflows").then(({ triggerRepoWorkflows }) => {
106
+ refs.forEach(refLine => {
107
+ const [oldSha, newSha, refName] = refLine.split(" ");
108
+ // Only trigger on branch updates (not deletions or tags for now unless supported)
109
+ if (newSha !== "0000000000000000000000000000000000000000" && refName.startsWith("refs/heads/")) {
110
+ triggerRepoWorkflows(repo.id, newSha, refName, userId).catch(console.error);
111
+ }
112
+ });
113
+ });
114
+ }
115
+ }
116
+ });
117
+}
118
+
119
+main().catch(console.error);
0
120
▼scripts/test-gpg.ts
+25-0
1
+
2
+import * as openpgp from 'openpgp';
3
+
4
+async function test() {
5
+ try {
6
+ console.log("Generating key...");
7
+ const { privateKey, publicKey } = await openpgp.generateKey({
8
+ type: 'ecc',
9
+ curve: 'curve25519',
10
+ userIDs: [{ name: 'Test', email: '[email protected]' }],
11
+ });
12
+ console.log("Key generated.");
13
+
14
+ console.log("Reading public key...");
15
+ const key = await openpgp.readKey({ armoredKey: publicKey });
16
+ console.log("Key ID:", key.getKeyID().toHex());
17
+ console.log("Is Private:", key.isPrivate());
18
+
19
+ console.log("Test passed.");
20
+ } catch (e) {
21
+ console.error("Test failed:", e);
22
+ }
23
+}
24
+
25
+test();
0
26
▼src/components/admin/AdminDashboard.tsx
+158-0
1
+import { useEffect, useState, lazy, Suspense, Component, ReactNode } from "react";
2
+import DashboardStats from "./DashboardStats";
3
+import TrendingDevelopers from "./TrendingDevelopers";
4
+import LiveLogStream from "./LiveLogStream";
5
+import BottomPanel from "./BottomPanel";
6
+import SystemStatus from "./SystemStatus";
7
+import QuickStatsRow from "./QuickStatsRow";
8
+import AlertBanner from "./AlertBanner";
9
+import RecentActivity from "./RecentActivity";
10
+import { Github } from "lucide-react";
11
+
12
+// Error boundary for WebGL/Globe issues
13
+class GlobeErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
14
+ constructor(props: { children: ReactNode }) {
15
+ super(props);
16
+ this.state = { hasError: false };
17
+ }
18
+
19
+ static getDerivedStateFromError() {
20
+ return { hasError: true };
21
+ }
22
+
23
+ componentDidCatch(error: Error) {
24
+ console.warn("Globe component error:", error.message);
25
+ }
26
+
27
+ render() {
28
+ if (this.state.hasError) {
29
+ return (
30
+ <div className="w-full h-full flex items-center justify-center">
31
+ <div className="text-center text-white/50">
32
+ <div className="text-6xl mb-4">🌐</div>
33
+ <p className="text-sm">3D Globe unavailable</p>
34
+ <p className="text-xs mt-1 opacity-50">WebGL may not be supported</p>
35
+ </div>
36
+ </div>
37
+ );
38
+ }
39
+ return this.props.children;
40
+ }
41
+}
42
+
43
+const GlobeViz = lazy(() => import("./GlobeViz").catch(() => ({
44
+ default: () => (
45
+ <div className="w-full h-full flex items-center justify-center">
46
+ <div className="text-center text-white/50">
47
+ <div className="text-6xl mb-4">🌐</div>
48
+ <p className="text-sm">Globe failed to load</p>
49
+ </div>
50
+ </div>
51
+ )
52
+})));
53
+
54
+export default function AdminDashboard() {
55
+ const [data, setData] = useState<any>({
56
+ totalRepos: 0,
57
+ totalUsers: 0,
58
+ collaborations: 0,
59
+ trendingDevelopers: [],
60
+ activityLog: [],
61
+ codeStats: {},
62
+ reviewStats: {},
63
+ languages: [],
64
+ quickStats: {}
65
+ });
66
+
67
+ useEffect(() => {
68
+ const fetchData = async () => {
69
+ try {
70
+ const res = await fetch("/api/admin/stats");
71
+ const json = await res.json();
72
+ setData(json);
73
+ } catch (e) {
74
+ console.error("Failed to fetch admin stats");
75
+ }
76
+ };
77
+
78
+ fetchData();
79
+ const interval = setInterval(fetchData, 10000);
80
+ return () => clearInterval(interval);
81
+ }, []);
82
+
83
+ return (
84
+ <div className="relative h-full w-full">
85
+ {/* 3D Globe Background */}
86
+ <div className="absolute inset-0 z-0 opacity-80 pointer-events-auto">
87
+ <GlobeErrorBoundary>
88
+ <Suspense fallback={
89
+ <div className="w-full h-full flex items-center justify-center">
90
+ <div className="text-white/30 text-sm">Loading globe...</div>
91
+ </div>
92
+ }>
93
+ <GlobeViz activities={data.activityLog || []} />
94
+ </Suspense>
95
+ </GlobeErrorBoundary>
96
+ </div>
97
+
98
+ <div className="relative z-10 flex flex-col h-full p-6 pb-8 pointer-events-none">
99
+
100
+ {/* Top Alert Banner */}
101
+ <div className="mb-4 pointer-events-auto shrink-0">
102
+ <AlertBanner />
103
+ </div>
104
+
105
+ {/* Quick Stats Row - Top Center */}
106
+ <div className="mb-4 pointer-events-auto shrink-0 flex justify-center">
107
+ <QuickStatsRow stats={data.quickStats || {}} />
108
+ </div>
109
+
110
+ {/* Main Content Grid */}
111
+ <div className="flex-1 grid grid-cols-12 gap-6 min-h-0">
112
+ {/* Left Column: Stats & Trending */}
113
+ <div className="col-span-3 flex flex-col gap-4 pointer-events-auto overflow-hidden">
114
+ {/* Stats Block */}
115
+ <div className="glass-panel rounded-xl shrink-0">
116
+ <DashboardStats stats={data} />
117
+ </div>
118
+
119
+ {/* Trending Devs Block */}
120
+ <div className="glass-panel rounded-xl flex-1 overflow-y-auto custom-scrollbar min-h-0">
121
+ <TrendingDevelopers developers={data.trendingDevelopers || []} />
122
+ </div>
123
+ </div>
124
+
125
+ {/* Middle Column: Globe Area with System Status */}
126
+ <div className="col-span-6 flex flex-col justify-between items-center pointer-events-none py-4">
127
+ {/* Top area - Recent Activity floating */}
128
+ <div className="pointer-events-auto self-start">
129
+ <RecentActivity activities={data.recentActivity || []} />
130
+ </div>
131
+
132
+ {/* Bottom - System Status HUD */}
133
+ <div className="pointer-events-auto">
134
+ <SystemStatus status={data.systemStatus} />
135
+ </div>
136
+ </div>
137
+
138
+ {/* Right Column: Log Stream */}
139
+ <div className="col-span-3 flex flex-col gap-4 pointer-events-auto overflow-hidden">
140
+ <div className="flex justify-end shrink-0">
141
+ <Github className="h-8 w-8 text-white opacity-30 hover:opacity-70 transition-opacity cursor-pointer" />
142
+ </div>
143
+
144
+ <div className="glass-panel rounded-xl flex-1 overflow-hidden min-h-0 relative">
145
+ <LiveLogStream logs={data.activityLog || []} />
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ {/* Bottom Section: Carousel Panel */}
151
+ <div className="h-48 shrink-0 pointer-events-auto mt-4">
152
+ <BottomPanel stats={data} />
153
+ </div>
154
+
155
+ </div>
156
+ </div>
157
+ );
158
+}
0
159
▼src/components/admin/AlertBanner.tsx
+108-0
1
+import { motion } from "framer-motion";
2
+import { AlertTriangle, CheckCircle, Info, Bell, X } from "lucide-react";
3
+import { useState } from "react";
4
+
5
+interface Alert {
6
+ id: string;
7
+ type: "success" | "warning" | "info" | "error";
8
+ message: string;
9
+ timestamp: string;
10
+}
11
+
12
+interface Props {
13
+ alerts?: Alert[];
14
+}
15
+
16
+const alertStyles = {
17
+ success: {
18
+ bg: "from-green-500/10 via-emerald-500/5 to-transparent",
19
+ border: "border-green-500/30",
20
+ icon: CheckCircle,
21
+ iconColor: "text-green-400",
22
+ glow: "shadow-green-500/20"
23
+ },
24
+ warning: {
25
+ bg: "from-yellow-500/10 via-amber-500/5 to-transparent",
26
+ border: "border-yellow-500/30",
27
+ icon: AlertTriangle,
28
+ iconColor: "text-yellow-400",
29
+ glow: "shadow-yellow-500/20"
30
+ },
31
+ info: {
32
+ bg: "from-blue-500/10 via-cyan-500/5 to-transparent",
33
+ border: "border-blue-500/30",
34
+ icon: Info,
35
+ iconColor: "text-blue-400",
36
+ glow: "shadow-blue-500/20"
37
+ },
38
+ error: {
39
+ bg: "from-red-500/10 via-rose-500/5 to-transparent",
40
+ border: "border-red-500/30",
41
+ icon: AlertTriangle,
42
+ iconColor: "text-red-400",
43
+ glow: "shadow-red-500/20"
44
+ }
45
+};
46
+
47
+const defaultAlerts: Alert[] = [
48
+ {
49
+ id: "1",
50
+ type: "success",
51
+ message: "All systems operational • 99.9% uptime this month",
52
+ timestamp: "Just now"
53
+ }
54
+];
55
+
56
+export default function AlertBanner({ alerts = defaultAlerts }: Props) {
57
+ const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set());
58
+
59
+ const visibleAlerts = alerts.filter(a => !dismissedIds.has(a.id));
60
+
61
+ if (visibleAlerts.length === 0) return null;
62
+
63
+ const alert = visibleAlerts[0];
64
+ const style = alertStyles[alert.type];
65
+ const Icon = style.icon;
66
+
67
+ return (
68
+ <motion.div
69
+ initial={{ opacity: 0, y: -10 }}
70
+ animate={{ opacity: 1, y: 0 }}
71
+ exit={{ opacity: 0, y: -10 }}
72
+ className="w-full max-w-2xl mx-auto"
73
+ >
74
+ <div className={`relative overflow-hidden rounded-lg border ${style.border} bg-gradient-to-r ${style.bg} backdrop-blur-sm shadow-lg ${style.glow}`}>
75
+ {/* Animated shimmer */}
76
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent animate-shimmer" />
77
+
78
+ <div className="relative flex items-center gap-3 px-4 py-2.5">
79
+ {/* Pulsing icon */}
80
+ <motion.div
81
+ animate={{ scale: [1, 1.1, 1] }}
82
+ transition={{ duration: 2, repeat: Infinity }}
83
+ >
84
+ <Icon className={`w-4 h-4 ${style.iconColor}`} />
85
+ </motion.div>
86
+
87
+ {/* Message */}
88
+ <span className="flex-1 text-sm text-gray-200">
89
+ {alert.message}
90
+ </span>
91
+
92
+ {/* Timestamp */}
93
+ <span className="text-xs text-gray-500 font-mono">
94
+ {alert.timestamp}
95
+ </span>
96
+
97
+ {/* Dismiss button */}
98
+ <button
99
+ onClick={() => setDismissedIds(prev => new Set([...prev, alert.id]))}
100
+ className="p-1 rounded hover:bg-white/10 transition-colors"
101
+ >
102
+ <X className="w-3 h-3 text-gray-500 hover:text-gray-300" />
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </motion.div>
107
+ );
108
+}
0
109
▼src/components/admin/BottomPanel.tsx
+311-0
1
+import { useState, useEffect } from "react";
2
+import { motion, AnimatePresence } from "framer-motion";
3
+import { Code2, GitCommit, MessageSquare, Users, TrendingUp, TrendingDown, Sparkles, Globe } from "lucide-react";
4
+
5
+interface Props {
6
+ stats: any;
7
+}
8
+
9
+// Animated number component
10
+function AnimatedNumber({ value, className, prefix = "", suffix = "" }: {
11
+ value: number;
12
+ className?: string;
13
+ prefix?: string;
14
+ suffix?: string;
15
+}) {
16
+ const [displayValue, setDisplayValue] = useState(0);
17
+
18
+ useEffect(() => {
19
+ const duration = 1200;
20
+ const startTime = Date.now();
21
+ const startValue = displayValue;
22
+
23
+ const animate = () => {
24
+ const now = Date.now();
25
+ const progress = Math.min((now - startTime) / duration, 1);
26
+ const eased = 1 - Math.pow(1 - progress, 3);
27
+ setDisplayValue(Math.floor(startValue + (value - startValue) * eased));
28
+
29
+ if (progress < 1) {
30
+ requestAnimationFrame(animate);
31
+ }
32
+ };
33
+
34
+ requestAnimationFrame(animate);
35
+ }, [value]);
36
+
37
+ return <span className={className}>{prefix}{displayValue.toLocaleString()}{suffix}</span>;
38
+}
39
+
40
+export default function BottomPanel({ stats }: Props) {
41
+ const [activeSlide, setActiveSlide] = useState(0);
42
+
43
+ const SLIDES = 4;
44
+
45
+ useEffect(() => {
46
+ const interval = setInterval(() => {
47
+ setActiveSlide((prev) => (prev + 1) % SLIDES);
48
+ }, 15000);
49
+ return () => clearInterval(interval);
50
+ }, []);
51
+
52
+ const variants = {
53
+ enter: { opacity: 0, y: 30, filter: "blur(10px)" },
54
+ center: { opacity: 1, y: 0, filter: "blur(0px)" },
55
+ exit: { opacity: 0, y: -30, filter: "blur(10px)" },
56
+ };
57
+
58
+ return (
59
+ <div className="relative rounded-2xl h-48 overflow-hidden flex items-center w-full">
60
+ {/* Animated gradient border */}
61
+ <div className="absolute inset-0 rounded-2xl p-[1px] bg-gradient-to-r from-cyan-500/50 via-purple-500/50 to-pink-500/50 animate-gradient-x bg-[length:200%_auto]">
62
+ <div className="h-full w-full rounded-2xl bg-[#050510]" />
63
+ </div>
64
+
65
+ {/* Content */}
66
+ <div className="relative h-full w-full p-6 glass-panel rounded-2xl">
67
+ <AnimatePresence mode="wait">
68
+
69
+ {/* VIEW 1: Code Frequency */}
70
+ {activeSlide === 0 && (
71
+ <motion.div
72
+ key="code-freq"
73
+ variants={variants}
74
+ initial="enter"
75
+ animate="center"
76
+ exit="exit"
77
+ transition={{ duration: 0.5 }}
78
+ className="w-full h-full flex items-center justify-between px-8"
79
+ >
80
+ <div className="relative">
81
+ {/* Glow effect */}
82
+ <div className="absolute -inset-4 bg-gradient-to-r from-green-500/20 to-emerald-500/20 rounded-xl blur-xl" />
83
+
84
+ <div className="relative">
85
+ <div className="text-xs uppercase tracking-widest text-emerald-400/60 mb-3 flex items-center gap-2">
86
+ <Code2 className="h-4 w-4 text-emerald-400" />
87
+ Code Lines Added / Deleted
88
+ </div>
89
+ <div className="flex items-end gap-5">
90
+ <div className="flex items-center gap-2">
91
+ <TrendingUp className="h-6 w-6 text-green-400" />
92
+ <span className="text-5xl font-mono font-light bg-gradient-to-r from-green-300 to-emerald-400 bg-clip-text text-transparent">
93
+ <AnimatedNumber value={stats.codeStats?.added || 124475} prefix="+" />
94
+ </span>
95
+ </div>
96
+ <div className="flex items-center gap-2 mb-1">
97
+ <TrendingDown className="h-5 w-5 text-red-400 opacity-60" />
98
+ <span className="text-3xl font-mono text-red-400/70 font-light">
99
+ <AnimatedNumber value={stats.codeStats?.deleted || 48468} prefix="-" />
100
+ </span>
101
+ </div>
102
+ </div>
103
+ <div className="text-xs text-gray-500 mt-3 flex items-center gap-1.5">
104
+ <Globe className="h-3 w-3" />
105
+ Global repository statistics (Last 24h)
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ {/* Contribution bars */}
111
+ <div className="flex gap-1 items-end h-24">
112
+ {Array.from({ length: 45 }).map((_, i) => {
113
+ const height = Math.random() * 100;
114
+ return (
115
+ <motion.div
116
+ key={i}
117
+ className="w-1.5 rounded-full bg-gradient-to-t from-green-600 to-emerald-400"
118
+ initial={{ height: 0 }}
119
+ animate={{ height: `${height}%` }}
120
+ transition={{
121
+ delay: i * 0.02,
122
+ duration: 0.8,
123
+ ease: "easeOut"
124
+ }}
125
+ style={{ opacity: 0.3 + (height / 100) * 0.7 }}
126
+ />
127
+ );
128
+ })}
129
+ </div>
130
+ </motion.div>
131
+ )}
132
+
133
+ {/* VIEW 2: Contribution Graph */}
134
+ {activeSlide === 1 && (
135
+ <motion.div
136
+ key="contrib-graph"
137
+ variants={variants}
138
+ initial="enter"
139
+ animate="center"
140
+ exit="exit"
141
+ transition={{ duration: 0.5 }}
142
+ className="w-full h-full flex flex-col justify-center px-8"
143
+ >
144
+ <div className="text-xs uppercase tracking-widest text-blue-400/60 mb-4 flex items-center gap-2">
145
+ <GitCommit className="h-4 w-4 text-blue-400" />
146
+ System-wide Contribution Graph
147
+ </div>
148
+ <div className="flex gap-1 flex-wrap justify-center">
149
+ {Array.from({ length: 168 }).map((_, i) => {
150
+ const level = Math.floor(Math.random() * 5);
151
+ const colors = ['#161b22', '#0e4429', '#006d32', '#26a641', '#39d353'];
152
+ return (
153
+ <motion.div
154
+ key={i}
155
+ className="w-3 h-3 rounded-sm"
156
+ initial={{ opacity: 0, scale: 0 }}
157
+ animate={{ opacity: 1, scale: 1 }}
158
+ transition={{ delay: i * 0.005 }}
159
+ style={{ backgroundColor: colors[level] }}
160
+ />
161
+ );
162
+ })}
163
+ </div>
164
+ </motion.div>
165
+ )}
166
+
167
+ {/* VIEW 3: User Engagement */}
168
+ {activeSlide === 2 && (
169
+ <motion.div
170
+ key="engagement"
171
+ variants={variants}
172
+ initial="enter"
173
+ animate="center"
174
+ exit="exit"
175
+ transition={{ duration: 0.5 }}
176
+ className="w-full h-full flex items-center justify-between px-8"
177
+ >
178
+ <div className="relative">
179
+ <div className="absolute -inset-4 bg-gradient-to-r from-purple-500/20 to-violet-500/20 rounded-xl blur-xl" />
180
+ <div className="relative">
181
+ <div className="text-xs uppercase tracking-widest text-purple-400/60 mb-3 flex items-center gap-2">
182
+ <Users className="h-4 w-4 text-purple-400" />
183
+ Users Reviewing & Commenting
184
+ </div>
185
+ <div className="text-5xl font-mono font-light bg-gradient-to-r from-purple-300 via-violet-400 to-fuchsia-400 bg-clip-text text-transparent">
186
+ <AnimatedNumber value={stats.reviewStats?.count || 156} />
187
+ </div>
188
+ <div className="text-xs text-gray-500 mt-3 flex items-center gap-1.5">
189
+ <Sparkles className="h-3 w-3 text-purple-400/60" />
190
+ Active participants in code review
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ {/* Timeline */}
196
+ <div className="flex-1 px-16 flex items-center justify-center relative">
197
+ <div className="absolute w-full h-[2px] bg-gradient-to-r from-transparent via-purple-500/30 to-transparent" />
198
+ <div className="flex gap-10 relative z-10">
199
+ {Array.from({ length: 6 }).map((_, i) => {
200
+ const colors = ['#8957e5', '#238636', '#da3633', '#e3b341', '#3fb950', '#f778ba'];
201
+ return (
202
+ <motion.div
203
+ key={i}
204
+ className="h-4 w-4 rounded-full relative"
205
+ initial={{ scale: 0 }}
206
+ animate={{ scale: 1 }}
207
+ transition={{ delay: 0.3 + i * 0.1, type: "spring" }}
208
+ style={{ backgroundColor: colors[i % colors.length] }}
209
+ >
210
+ <div
211
+ className="absolute inset-0 rounded-full animate-ping"
212
+ style={{
213
+ backgroundColor: colors[i % colors.length],
214
+ animationDelay: `${i * 0.2}s`,
215
+ animationDuration: "2s"
216
+ }}
217
+ />
218
+ </motion.div>
219
+ );
220
+ })}
221
+ </div>
222
+ </div>
223
+
224
+ {/* Avatars */}
225
+ <div className="flex items-center -space-x-3">
226
+ {(stats.reviewStats?.activeReviewers || Array.from({ length: 4 })).slice(0, 5).map((u: any, i: number) => (
227
+ <motion.div
228
+ key={i}
229
+ className="h-12 w-12 rounded-full border-2 border-[#050510] overflow-hidden bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center"
230
+ initial={{ scale: 0, x: 20 }}
231
+ animate={{ scale: 1, x: 0 }}
232
+ transition={{ delay: 0.2 + i * 0.1 }}
233
+ >
234
+ {u?.avatar ? (
235
+ <img src={u.avatar} className="h-full w-full object-cover" />
236
+ ) : (
237
+ <span className="text-sm font-bold text-white">{String.fromCharCode(65 + i)}</span>
238
+ )}
239
+ </motion.div>
240
+ ))}
241
+ </div>
242
+ </motion.div>
243
+ )}
244
+
245
+ {/* VIEW 4: Languages */}
246
+ {activeSlide === 3 && (
247
+ <motion.div
248
+ key="languages"
249
+ variants={variants}
250
+ initial="enter"
251
+ animate="center"
252
+ exit="exit"
253
+ transition={{ duration: 0.5 }}
254
+ className="w-full h-full flex flex-col justify-center px-8"
255
+ >
256
+ <div className="text-xs uppercase tracking-widest text-orange-400/60 mb-4 flex items-center gap-2">
257
+ <MessageSquare className="h-4 w-4 text-orange-400" />
258
+ Usage by Language
259
+ </div>
260
+ <div className="space-y-3 max-w-4xl w-full">
261
+ {(stats.languages || []).map((lang: any, i: number) => (
262
+ <div key={lang.name} className="flex items-center gap-4">
263
+ <div className="w-24 text-sm text-gray-300 font-mono">{lang.name}</div>
264
+ <div className="flex-1 h-2 bg-white/5 rounded-full overflow-hidden">
265
+ <motion.div
266
+ initial={{ width: 0 }}
267
+ animate={{ width: `${lang.percentage}%` }}
268
+ transition={{ delay: i * 0.15, duration: 0.8, ease: "easeOut" }}
269
+ className="h-full rounded-full relative overflow-hidden"
270
+ style={{ backgroundColor: lang.color }}
271
+ >
272
+ {/* Shimmer effect */}
273
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer" />
274
+ </motion.div>
275
+ </div>
276
+ <div className="w-12 text-sm text-gray-400 text-right font-mono">{lang.percentage}%</div>
277
+ </div>
278
+ ))}
279
+ </div>
280
+ </motion.div>
281
+ )}
282
+
283
+ </AnimatePresence>
284
+
285
+ {/* Progress indicators with glow */}
286
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2">
287
+ {[0, 1, 2, 3].map(i => (
288
+ <button
289
+ key={i}
290
+ onClick={() => setActiveSlide(i)}
291
+ className="relative"
292
+ >
293
+ {activeSlide === i && (
294
+ <motion.div
295
+ layoutId="activeIndicator"
296
+ className="absolute -inset-1 bg-blue-500/30 rounded-full blur-sm"
297
+ />
298
+ )}
299
+ <div
300
+ className={`relative h-1.5 rounded-full transition-all duration-300 ${activeSlide === i
301
+ ? 'w-8 bg-gradient-to-r from-cyan-400 to-blue-500'
302
+ : 'w-2 bg-gray-700 hover:bg-gray-600'
303
+ }`}
304
+ />
305
+ </button>
306
+ ))}
307
+ </div>
308
+ </div>
309
+ </div>
310
+ );
311
+}
0
312
▼src/components/admin/DashboardStats.tsx
+108-0
1
+import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
2
+import { useEffect, useState } from "react";
3
+import { Database, GitPullRequest, Sparkles } from "lucide-react";
4
+
5
+interface Props {
6
+ stats: {
7
+ totalRepos: number;
8
+ collaborations: number;
9
+ }
10
+}
11
+
12
+// Animated counter that smoothly transitions between numbers
13
+function AnimatedNumber({ value, className }: { value: number; className?: string }) {
14
+ const [displayValue, setDisplayValue] = useState(0);
15
+
16
+ useEffect(() => {
17
+ const duration = 1500;
18
+ const startTime = Date.now();
19
+ const startValue = displayValue;
20
+
21
+ const animate = () => {
22
+ const now = Date.now();
23
+ const progress = Math.min((now - startTime) / duration, 1);
24
+ // Easing function for smooth animation
25
+ const eased = 1 - Math.pow(1 - progress, 3);
26
+ setDisplayValue(Math.floor(startValue + (value - startValue) * eased));
27
+
28
+ if (progress < 1) {
29
+ requestAnimationFrame(animate);
30
+ }
31
+ };
32
+
33
+ requestAnimationFrame(animate);
34
+ }, [value]);
35
+
36
+ return <span className={className}>{displayValue.toLocaleString()}</span>;
37
+}
38
+
39
+export default function DashboardStats({ stats }: Props) {
40
+ return (
41
+ <div className="flex flex-col gap-8 p-6">
42
+ {/* Total Repositories - with glowing effect */}
43
+ <motion.div
44
+ initial={{ opacity: 0, x: -20 }}
45
+ animate={{ opacity: 1, x: 0 }}
46
+ transition={{ duration: 0.5 }}
47
+ className="relative group"
48
+ >
49
+ {/* Glow effect on hover */}
50
+ <div className="absolute -inset-2 bg-gradient-to-r from-cyan-500/20 via-blue-500/20 to-purple-500/20 rounded-lg blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
51
+
52
+ <div className="relative">
53
+ <div className="flex items-center gap-2 mb-2">
54
+ <Database className="w-4 h-4 text-cyan-400" />
55
+ <h3 className="text-sm uppercase tracking-widest text-cyan-400/80">
56
+ Total Repositories
57
+ </h3>
58
+ </div>
59
+ <div className="text-5xl font-light font-mono">
60
+ <span className="bg-gradient-to-r from-white via-cyan-200 to-cyan-400 bg-clip-text text-transparent">
61
+ <AnimatedNumber value={stats.totalRepos} />
62
+ </span>
63
+ </div>
64
+ <div className="flex items-center gap-1.5 mt-2">
65
+ <motion.span
66
+ className="relative flex h-2 w-2"
67
+ animate={{ scale: [1, 1.2, 1] }}
68
+ transition={{ duration: 2, repeat: Infinity }}
69
+ >
70
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
71
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-500" />
72
+ </motion.span>
73
+ <span className="text-xs text-cyan-400/60">Live Count</span>
74
+ </div>
75
+ </div>
76
+ </motion.div>
77
+
78
+ {/* Active Collaborations - with pink/rose theme */}
79
+ <motion.div
80
+ initial={{ opacity: 0, x: -20 }}
81
+ animate={{ opacity: 1, x: 0 }}
82
+ transition={{ duration: 0.5, delay: 0.1 }}
83
+ className="relative group"
84
+ >
85
+ {/* Glow effect on hover */}
86
+ <div className="absolute -inset-2 bg-gradient-to-r from-pink-500/20 via-rose-500/20 to-orange-500/20 rounded-lg blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
87
+
88
+ <div className="relative">
89
+ <div className="flex items-center gap-2 mb-2">
90
+ <GitPullRequest className="w-4 h-4 text-rose-400" />
91
+ <h3 className="text-sm uppercase tracking-widest text-rose-400/80">
92
+ Active Collaborations
93
+ </h3>
94
+ </div>
95
+ <div className="text-5xl font-light font-mono">
96
+ <span className="bg-gradient-to-r from-rose-200 via-pink-300 to-rose-400 bg-clip-text text-transparent">
97
+ <AnimatedNumber value={stats.collaborations} />
98
+ </span>
99
+ </div>
100
+ <div className="flex items-center gap-1.5 mt-2">
101
+ <Sparkles className="w-3 h-3 text-rose-400/60" />
102
+ <span className="text-xs text-rose-400/60">Pull Requests</span>
103
+ </div>
104
+ </div>
105
+ </motion.div>
106
+ </div>
107
+ );
108
+}
0
109
▼src/components/admin/GlobeViz.tsx
+256-0
1
+"use client";
2
+import { useEffect, useRef, useState, Component, ReactNode } from "react";
3
+import createGlobe from "cobe";
4
+
5
+// Error boundary for WebGL issues
6
+class GlobeErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
7
+ constructor(props: { children: ReactNode }) {
8
+ super(props);
9
+ this.state = { hasError: false };
10
+ }
11
+
12
+ static getDerivedStateFromError() {
13
+ return { hasError: true };
14
+ }
15
+
16
+ componentDidCatch(error: Error) {
17
+ console.warn("GitHub Globe error:", error.message);
18
+ }
19
+
20
+ render() {
21
+ if (this.state.hasError) {
22
+ return (
23
+ <div className="w-full h-full flex items-center justify-center">
24
+ <div className="text-center text-white/50">
25
+ <div className="text-6xl mb-4">🌐</div>
26
+ <p className="text-sm">Globe unavailable</p>
27
+ </div>
28
+ </div>
29
+ );
30
+ }
31
+ return this.props.children;
32
+ }
33
+}
34
+
35
+// Check WebGL support
36
+function isWebGLSupported(): boolean {
37
+ try {
38
+ const canvas = document.createElement("canvas");
39
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
40
+ return !!gl;
41
+ } catch {
42
+ return false;
43
+ }
44
+}
45
+
46
+interface Activity {
47
+ user: string;
48
+ repo: string;
49
+ type: string;
50
+}
51
+
52
+interface Props {
53
+ activities?: Activity[];
54
+}
55
+
56
+function GlobeContent({ activities = [] }: Props) {
57
+ const canvasRef = useRef<HTMLCanvasElement>(null);
58
+ const pointerInteracting = useRef<number | null>(null);
59
+ const pointerInteractionMovement = useRef(0);
60
+ const [rotation, setRotation] = useState(0);
61
+
62
+ useEffect(() => {
63
+ if (!canvasRef.current) return;
64
+
65
+ let phi = 0;
66
+ let width = 0;
67
+
68
+ const onResize = () => {
69
+ if (canvasRef.current) {
70
+ width = canvasRef.current.offsetWidth;
71
+ }
72
+ };
73
+ window.addEventListener("resize", onResize);
74
+ onResize();
75
+
76
+ const globe = createGlobe(canvasRef.current, {
77
+ devicePixelRatio: 2,
78
+ width: width * 2,
79
+ height: width * 2,
80
+ phi: 0,
81
+ theta: 0.3,
82
+ dark: 1,
83
+ diffuse: 1.2,
84
+ mapSamples: 16000,
85
+ mapBrightness: 6,
86
+ baseColor: [0.3, 0.3, 0.3],
87
+ markerColor: [0.1, 0.8, 1],
88
+ glowColor: [0.1, 0.1, 0.3],
89
+ markers: activities.slice(0, 20).map((_, i) => ({
90
+ location: [
91
+ (Math.random() - 0.5) * 180,
92
+ (Math.random() - 0.5) * 360
93
+ ],
94
+ size: 0.03 + Math.random() * 0.05,
95
+ })),
96
+ onRender: (state) => {
97
+ if (!pointerInteracting.current) {
98
+ phi += 0.005;
99
+ }
100
+ state.phi = phi + rotation;
101
+ state.width = width * 2;
102
+ state.height = width * 2;
103
+ },
104
+ });
105
+
106
+ setTimeout(() => {
107
+ if (canvasRef.current) {
108
+ canvasRef.current.style.opacity = "1";
109
+ }
110
+ }, 100);
111
+
112
+ return () => {
113
+ globe.destroy();
114
+ window.removeEventListener("resize", onResize);
115
+ };
116
+ }, [activities, rotation]);
117
+
118
+ return (
119
+ <div className="w-full h-full flex items-center justify-center cursor-move">
120
+ <canvas
121
+ ref={canvasRef}
122
+ onPointerDown={(e) => {
123
+ pointerInteracting.current = e.clientX - pointerInteractionMovement.current;
124
+ if (canvasRef.current) {
125
+ canvasRef.current.style.cursor = "grabbing";
126
+ }
127
+ }}
128
+ onPointerUp={() => {
129
+ pointerInteracting.current = null;
130
+ if (canvasRef.current) {
131
+ canvasRef.current.style.cursor = "grab";
132
+ }
133
+ }}
134
+ onPointerOut={() => {
135
+ pointerInteracting.current = null;
136
+ if (canvasRef.current) {
137
+ canvasRef.current.style.cursor = "grab";
138
+ }
139
+ }}
140
+ onMouseMove={(e) => {
141
+ if (pointerInteracting.current !== null) {
142
+ const delta = e.clientX - pointerInteracting.current;
143
+ pointerInteractionMovement.current = delta;
144
+ setRotation(delta / 200);
145
+ }
146
+ }}
147
+ onTouchMove={(e) => {
148
+ if (pointerInteracting.current !== null && e.touches[0]) {
149
+ const delta = e.touches[0].clientX - pointerInteracting.current;
150
+ pointerInteractionMovement.current = delta;
151
+ setRotation(delta / 100);
152
+ }
153
+ }}
154
+ style={{
155
+ width: "100%",
156
+ height: "100%",
157
+ maxWidth: "600px",
158
+ aspectRatio: "1",
159
+ opacity: 0,
160
+ transition: "opacity 1s ease",
161
+ cursor: "grab",
162
+ }}
163
+ />
164
+ </div>
165
+ );
166
+}
167
+
168
+export default function GlobeViz({ activities = [] }: Props) {
169
+ const [webglSupported, setWebglSupported] = useState<boolean | null>(null);
170
+ const [mounted, setMounted] = useState(false);
171
+
172
+ useEffect(() => {
173
+ setMounted(true);
174
+ setWebglSupported(isWebGLSupported());
175
+ }, []);
176
+
177
+ // Still loading
178
+ if (!mounted || webglSupported === null) {
179
+ return (
180
+ <div className="w-full h-full flex items-center justify-center">
181
+ <div className="text-white/30 text-sm">Loading globe...</div>
182
+ </div>
183
+ );
184
+ }
185
+
186
+ // No WebGL support - show CSS fallback
187
+ if (!webglSupported) {
188
+ return <CSSGlobeFallback activities={activities} />;
189
+ }
190
+
191
+ return (
192
+ <GlobeErrorBoundary>
193
+ <GlobeContent activities={activities} />
194
+ </GlobeErrorBoundary>
195
+ );
196
+}
197
+
198
+// CSS-based fallback for systems without WebGL
199
+function CSSGlobeFallback({ activities = [] }: Props) {
200
+ const [points, setPoints] = useState<Array<{ x: number; y: number; delay: number }>>([]);
201
+
202
+ useEffect(() => {
203
+ const newPoints = Array.from({ length: 30 }).map(() => ({
204
+ x: Math.random() * 100,
205
+ y: Math.random() * 100,
206
+ delay: Math.random() * 3
207
+ }));
208
+ setPoints(newPoints);
209
+ }, []);
210
+
211
+ return (
212
+ <div className="w-full h-full flex items-center justify-center overflow-hidden">
213
+ <div className="relative w-[500px] h-[500px]">
214
+ {/* Outer glow */}
215
+ <div className="absolute inset-0 rounded-full bg-gradient-to-r from-blue-500/20 via-cyan-500/20 to-blue-500/20 blur-3xl animate-pulse" />
216
+
217
+ {/* Globe sphere */}
218
+ <div className="absolute inset-8 rounded-full bg-gradient-to-br from-slate-900 via-blue-900/30 to-slate-900 border border-cyan-500/20 shadow-2xl overflow-hidden">
219
+ {/* Grid lines */}
220
+ <div className="absolute inset-0 opacity-20">
221
+ {[...Array(8)].map((_, i) => (
222
+ <div
223
+ key={`h-${i}`}
224
+ className="absolute left-0 right-0 border-t border-cyan-400/30"
225
+ style={{ top: `${(i + 1) * 12}%` }}
226
+ />
227
+ ))}
228
+ </div>
229
+
230
+ {/* Rotating highlight */}
231
+ <div
232
+ className="absolute inset-0 bg-gradient-to-r from-transparent via-cyan-400/10 to-transparent animate-spin"
233
+ style={{ animationDuration: '20s' }}
234
+ />
235
+
236
+ {/* Activity points */}
237
+ {points.map((point, i) => (
238
+ <div
239
+ key={i}
240
+ className="absolute w-2 h-2 rounded-full bg-cyan-400 animate-ping"
241
+ style={{
242
+ left: `${point.x}%`,
243
+ top: `${point.y}%`,
244
+ animationDelay: `${point.delay}s`,
245
+ animationDuration: '2s'
246
+ }}
247
+ />
248
+ ))}
249
+ </div>
250
+
251
+ {/* Atmosphere */}
252
+ <div className="absolute inset-4 rounded-full border-2 border-cyan-400/20 animate-pulse" />
253
+ </div>
254
+ </div>
255
+ );
256
+}
0
257
▼src/components/admin/LiveLogStream.tsx
+143-0
1
+import { motion, AnimatePresence } from "framer-motion";
2
+import { Radio, Clock, GitCommit, GitPullRequest, GitFork, Star, MessageSquare } from "lucide-react";
3
+import { useEffect, useState } from "react";
4
+
5
+interface LogEntry {
6
+ id: string;
7
+ timestamp: string;
8
+ type: string;
9
+ message: string;
10
+ user: string;
11
+ repo: string;
12
+}
13
+
14
+interface Props {
15
+ logs: LogEntry[];
16
+}
17
+
18
+const getTypeIcon = (type: string) => {
19
+ const iconClass = "w-3 h-3";
20
+ switch (type.toLowerCase()) {
21
+ case "push": return <GitCommit className={`${iconClass} text-green-400`} />;
22
+ case "pr_open": return <GitPullRequest className={`${iconClass} text-blue-400`} />;
23
+ case "fork": return <GitFork className={`${iconClass} text-purple-400`} />;
24
+ case "star": return <Star className={`${iconClass} text-yellow-400`} />;
25
+ case "comment": return <MessageSquare className={`${iconClass} text-cyan-400`} />;
26
+ default: return <Radio className={`${iconClass} text-gray-400`} />;
27
+ }
28
+};
29
+
30
+const getTypeColor = (type: string) => {
31
+ switch (type.toLowerCase()) {
32
+ case "push": return "text-green-400";
33
+ case "pr_open": return "text-blue-400";
34
+ case "fork": return "text-purple-400";
35
+ case "star": return "text-yellow-400";
36
+ case "comment": return "text-cyan-400";
37
+ default: return "text-gray-400";
38
+ }
39
+};
40
+
41
+const getTypeBg = (type: string) => {
42
+ switch (type.toLowerCase()) {
43
+ case "push": return "from-green-500/10 to-transparent";
44
+ case "pr_open": return "from-blue-500/10 to-transparent";
45
+ case "fork": return "from-purple-500/10 to-transparent";
46
+ case "star": return "from-yellow-500/10 to-transparent";
47
+ case "comment": return "from-cyan-500/10 to-transparent";
48
+ default: return "from-gray-500/10 to-transparent";
49
+ }
50
+};
51
+
52
+export default function LiveLogStream({ logs }: Props) {
53
+ const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString());
54
+
55
+ useEffect(() => {
56
+ const interval = setInterval(() => {
57
+ setCurrentTime(new Date().toLocaleTimeString());
58
+ }, 1000);
59
+ return () => clearInterval(interval);
60
+ }, []);
61
+
62
+ return (
63
+ <div className="p-6 h-full flex flex-col">
64
+ {/* Header */}
65
+ <div className="flex justify-between items-center mb-4 pb-3 border-b border-white/5">
66
+ <div className="flex items-center gap-2">
67
+ <motion.div
68
+ animate={{ scale: [1, 1.2, 1] }}
69
+ transition={{ duration: 2, repeat: Infinity }}
70
+ >
71
+ <Radio className="w-4 h-4 text-red-400" />
72
+ </motion.div>
73
+ <div>
74
+ <h3 className="text-sm font-semibold bg-gradient-to-r from-gray-100 to-gray-400 bg-clip-text text-transparent">
75
+ Data Stream
76
+ </h3>
77
+ <div className="text-[10px] text-gray-500 uppercase tracking-wider">Real-Time</div>
78
+ </div>
79
+ </div>
80
+ <div className="flex items-center gap-2 text-xs text-gray-500 font-mono">
81
+ <Clock className="w-3 h-3" />
82
+ {currentTime}
83
+ </div>
84
+ </div>
85
+
86
+ {/* Log entries */}
87
+ <div className="flex-1 overflow-y-auto custom-scrollbar space-y-2">
88
+ <AnimatePresence initial={false}>
89
+ {logs.map((log, index) => (
90
+ <motion.div
91
+ key={log.id}
92
+ initial={{ opacity: 0, height: 0, x: 20 }}
93
+ animate={{ opacity: 1, height: "auto", x: 0 }}
94
+ exit={{ opacity: 0, height: 0, x: -20 }}
95
+ transition={{ type: "spring", stiffness: 200, damping: 20 }}
96
+ className={`relative overflow-hidden rounded-lg bg-gradient-to-r ${getTypeBg(log.type)} p-2`}
97
+ >
98
+ {/* Timestamp */}
99
+ <div className="text-[10px] text-gray-600 font-mono mb-1">
100
+ {log.timestamp}
101
+ </div>
102
+
103
+ {/* Content */}
104
+ <div className="flex items-start gap-2">
105
+ {/* Icon */}
106
+ <div className="mt-0.5">
107
+ {getTypeIcon(log.type)}
108
+ </div>
109
+
110
+ {/* Details */}
111
+ <div className="flex-1 min-w-0 text-xs">
112
+ <div className="flex flex-wrap gap-x-1.5 items-center">
113
+ <span className="text-gray-600 font-mono">#{log.id.substring(0, 7)}</span>
114
+ <span className="text-gray-600">:</span>
115
+ <span className={`font-semibold ${getTypeColor(log.type)}`}>
116
+ {log.type.toUpperCase().replace("_", " ")}
117
+ </span>
118
+ </div>
119
+ <div className="text-gray-400 truncate mt-0.5">
120
+ {log.message}
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ {/* Animated line for new entries */}
126
+ {index === 0 && (
127
+ <motion.div
128
+ className={`absolute left-0 top-0 bottom-0 w-0.5 bg-gradient-to-b ${getTypeColor(log.type).replace("text", "from")} to-transparent`}
129
+ initial={{ opacity: 0 }}
130
+ animate={{ opacity: [0, 1, 0] }}
131
+ transition={{ duration: 2 }}
132
+ />
133
+ )}
134
+ </motion.div>
135
+ ))}
136
+ </AnimatePresence>
137
+ </div>
138
+
139
+ {/* Fade effect at bottom */}
140
+ <div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-[#0a0a15] to-transparent pointer-events-none" />
141
+ </div>
142
+ );
143
+}
0
144
▼src/components/admin/QuickStatsRow.tsx
+130-0
1
+import { motion } from "framer-motion";
2
+import { GitCommit, GitMerge, CircleDot, Users, Clock, TrendingUp, Activity, Zap } from "lucide-react";
3
+import { useEffect, useState } from "react";
4
+
5
+interface Props {
6
+ stats: {
7
+ commitsToday?: number;
8
+ prsMerged?: number;
9
+ issuesClosed?: number;
10
+ activeUsers?: number;
11
+ };
12
+}
13
+
14
+function AnimatedNumber({ value }: { value: number }) {
15
+ const [display, setDisplay] = useState(0);
16
+
17
+ useEffect(() => {
18
+ const duration = 1000;
19
+ const start = Date.now();
20
+ const startVal = display;
21
+
22
+ const tick = () => {
23
+ const elapsed = Date.now() - start;
24
+ const progress = Math.min(elapsed / duration, 1);
25
+ const eased = 1 - Math.pow(1 - progress, 3);
26
+ setDisplay(Math.floor(startVal + (value - startVal) * eased));
27
+ if (progress < 1) requestAnimationFrame(tick);
28
+ };
29
+
30
+ requestAnimationFrame(tick);
31
+ }, [value]);
32
+
33
+ return <>{display.toLocaleString()}</>;
34
+}
35
+
36
+const widgets = [
37
+ {
38
+ icon: GitCommit,
39
+ label: "Commits Today",
40
+ key: "commitsToday",
41
+ color: "from-green-400 to-emerald-500",
42
+ iconColor: "text-green-400",
43
+ bgGlow: "bg-green-500/20"
44
+ },
45
+ {
46
+ icon: GitMerge,
47
+ label: "PRs Merged",
48
+ key: "prsMerged",
49
+ color: "from-purple-400 to-violet-500",
50
+ iconColor: "text-purple-400",
51
+ bgGlow: "bg-purple-500/20"
52
+ },
53
+ {
54
+ icon: CircleDot,
55
+ label: "Issues Closed",
56
+ key: "issuesClosed",
57
+ color: "from-orange-400 to-amber-500",
58
+ iconColor: "text-orange-400",
59
+ bgGlow: "bg-orange-500/20"
60
+ },
61
+ {
62
+ icon: Users,
63
+ label: "Active Users",
64
+ key: "activeUsers",
65
+ color: "from-cyan-400 to-blue-500",
66
+ iconColor: "text-cyan-400",
67
+ bgGlow: "bg-cyan-500/20"
68
+ }
69
+];
70
+
71
+export default function QuickStatsRow({ stats }: Props) {
72
+ // Use real data from API only, default to 0
73
+ const data = {
74
+ commitsToday: stats.commitsToday ?? 0,
75
+ prsMerged: stats.prsMerged ?? 0,
76
+ issuesClosed: stats.issuesClosed ?? 0,
77
+ activeUsers: stats.activeUsers ?? 0
78
+ };
79
+
80
+ return (
81
+ <div className="flex items-center justify-center gap-4">
82
+ {widgets.map((widget, i) => {
83
+ const Icon = widget.icon;
84
+ const value = data[widget.key as keyof typeof data];
85
+
86
+ return (
87
+ <motion.div
88
+ key={widget.key}
89
+ initial={{ opacity: 0, y: -20 }}
90
+ animate={{ opacity: 1, y: 0 }}
91
+ transition={{ delay: i * 0.1, type: "spring", stiffness: 100 }}
92
+ className="relative group"
93
+ >
94
+ {/* Glow effect */}
95
+ <div className={`absolute -inset-1 ${widget.bgGlow} rounded-xl blur-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300`} />
96
+
97
+ <div className="relative glass-panel rounded-xl px-4 py-3 flex items-center gap-3 hover:border-white/10 transition-colors cursor-default">
98
+ {/* Icon with gradient background */}
99
+ <div className={`w-10 h-10 rounded-lg bg-gradient-to-br ${widget.color} p-[1px]`}>
100
+ <div className="w-full h-full rounded-lg bg-[#0a0a15] flex items-center justify-center">
101
+ <Icon className={`w-5 h-5 ${widget.iconColor}`} />
102
+ </div>
103
+ </div>
104
+
105
+ {/* Stats */}
106
+ <div>
107
+ <div className={`text-xl font-bold font-mono bg-gradient-to-r ${widget.color} bg-clip-text text-transparent`}>
108
+ <AnimatedNumber value={value} />
109
+ </div>
110
+ <div className="text-[10px] uppercase tracking-wider text-gray-500">
111
+ {widget.label}
112
+ </div>
113
+ </div>
114
+
115
+ {/* Trend indicator */}
116
+ <div className="ml-2">
117
+ <motion.div
118
+ animate={{ y: [0, -2, 0] }}
119
+ transition={{ duration: 2, repeat: Infinity }}
120
+ >
121
+ <TrendingUp className={`w-3 h-3 ${widget.iconColor} opacity-60`} />
122
+ </motion.div>
123
+ </div>
124
+ </div>
125
+ </motion.div>
126
+ );
127
+ })}
128
+ </div>
129
+ );
130
+}
0
131
▼src/components/admin/RecentActivity.tsx
+91-0
1
+import { motion } from "framer-motion";
2
+import { GitCommit, GitPullRequest, GitFork, Star, User, Clock } from "lucide-react";
3
+
4
+interface Activity {
5
+ id: string;
6
+ type: "commit" | "pr" | "fork" | "star" | "user";
7
+ user: string;
8
+ action: string;
9
+ target: string;
10
+ time: string;
11
+}
12
+
13
+interface Props {
14
+ activities?: Activity[];
15
+}
16
+
17
+const activityIcons = {
18
+ commit: { icon: GitCommit, color: "text-green-400", bg: "bg-green-500/10" },
19
+ pr: { icon: GitPullRequest, color: "text-blue-400", bg: "bg-blue-500/10" },
20
+ fork: { icon: GitFork, color: "text-purple-400", bg: "bg-purple-500/10" },
21
+ star: { icon: Star, color: "text-yellow-400", bg: "bg-yellow-500/10" },
22
+ user: { icon: User, color: "text-cyan-400", bg: "bg-cyan-500/10" }
23
+};
24
+
25
+// Use real activities from API only
26
+export default function RecentActivity({ activities = [] }: Props) {
27
+ if (activities.length === 0) {
28
+ return (
29
+ <div className="glass-panel rounded-xl p-4 w-72">
30
+ <div className="flex items-center gap-2 mb-4">
31
+ <Clock className="w-4 h-4 text-cyan-400" />
32
+ <h3 className="text-sm font-semibold text-white">Recent Activity</h3>
33
+ </div>
34
+ <div className="text-center py-4 text-gray-500 text-sm">
35
+ No recent activity
36
+ </div>
37
+ </div>
38
+ );
39
+ }
40
+ return (
41
+ <div className="glass-panel rounded-xl p-4 w-72">
42
+ <div className="flex items-center gap-2 mb-4">
43
+ <motion.div
44
+ animate={{ rotate: 360 }}
45
+ transition={{ duration: 10, repeat: Infinity, ease: "linear" }}
46
+ >
47
+ <Clock className="w-4 h-4 text-cyan-400" />
48
+ </motion.div>
49
+ <h3 className="text-sm font-semibold text-white">Recent Activity</h3>
50
+ </div>
51
+
52
+ <div className="space-y-3">
53
+ {activities.slice(0, 4).map((activity, i) => {
54
+ const { icon: Icon, color, bg } = activityIcons[activity.type];
55
+
56
+ return (
57
+ <motion.div
58
+ key={activity.id}
59
+ initial={{ opacity: 0, x: -10 }}
60
+ animate={{ opacity: 1, x: 0 }}
61
+ transition={{ delay: i * 0.1 }}
62
+ className="flex items-start gap-3 group"
63
+ >
64
+ {/* Icon */}
65
+ <div className={`w-7 h-7 rounded-lg ${bg} flex items-center justify-center shrink-0`}>
66
+ <Icon className={`w-3.5 h-3.5 ${color}`} />
67
+ </div>
68
+
69
+ {/* Content */}
70
+ <div className="flex-1 min-w-0">
71
+ <div className="text-xs text-gray-300 truncate">
72
+ <span className="font-medium text-white">{activity.user}</span>
73
+ <span className="text-gray-500"> {activity.action} </span>
74
+ <span className="text-cyan-400">{activity.target}</span>
75
+ </div>
76
+ <div className="text-[10px] text-gray-600 mt-0.5">
77
+ {activity.time}
78
+ </div>
79
+ </div>
80
+
81
+ {/* Line connector */}
82
+ {i < activities.length - 1 && (
83
+ <div className="absolute left-[13px] top-8 w-[1px] h-4 bg-gradient-to-b from-gray-700 to-transparent" />
84
+ )}
85
+ </motion.div>
86
+ );
87
+ })}
88
+ </div>
89
+ </div>
90
+ );
91
+}
0
92
▼src/components/admin/SystemStatus.tsx
+176-0
1
+import { HardDrive, Cpu, Zap, Activity, Wifi, Server } from "lucide-react";
2
+import { motion } from "framer-motion";
3
+
4
+interface SystemStatusProps {
5
+ status?: {
6
+ cpuLoad: number;
7
+ memoryUsage: number;
8
+ memoryTotal: number;
9
+ storageUsage: number;
10
+ activeRunners: number;
11
+ uptime: string;
12
+ };
13
+}
14
+
15
+// Glowing ring gauge component
16
+function GlowingGauge({
17
+ value,
18
+ color,
19
+ icon: Icon,
20
+ label,
21
+ gradientColors
22
+}: {
23
+ value: number;
24
+ color: string;
25
+ icon: typeof Cpu;
26
+ label: string;
27
+ gradientColors: [string, string];
28
+}) {
29
+ const radius = 40;
30
+ const circumference = 2 * Math.PI * radius;
31
+ const strokeDashoffset = circumference - (circumference * value) / 100;
32
+
33
+ return (
34
+ <motion.div
35
+ className="relative w-28 h-28 flex items-center justify-center group"
36
+ initial={{ scale: 0.8, opacity: 0 }}
37
+ animate={{ scale: 1, opacity: 1 }}
38
+ transition={{ duration: 0.5, type: "spring" }}
39
+ >
40
+ {/* Outer glow */}
41
+ <div
42
+ className="absolute inset-0 rounded-full blur-xl opacity-40 group-hover:opacity-60 transition-opacity"
43
+ style={{ background: `radial-gradient(circle, ${gradientColors[0]}40, transparent 70%)` }}
44
+ />
45
+
46
+ <svg className="w-full h-full transform -rotate-90">
47
+ {/* Background circle */}
48
+ <circle
49
+ cx="56" cy="56" r={radius}
50
+ stroke="rgba(255,255,255,0.05)"
51
+ strokeWidth="8"
52
+ fill="transparent"
53
+ />
54
+ {/* Animated progress circle */}
55
+ <motion.circle
56
+ cx="56" cy="56" r={radius}
57
+ stroke={`url(#gradient-${label})`}
58
+ strokeWidth="8"
59
+ fill="transparent"
60
+ strokeLinecap="round"
61
+ initial={{ strokeDasharray: circumference, strokeDashoffset: circumference }}
62
+ animate={{ strokeDashoffset }}
63
+ transition={{ duration: 1.5, ease: "easeOut" }}
64
+ />
65
+ {/* Gradient definition */}
66
+ <defs>
67
+ <linearGradient id={`gradient-${label}`} x1="0%" y1="0%" x2="100%" y2="100%">
68
+ <stop offset="0%" stopColor={gradientColors[0]} />
69
+ <stop offset="100%" stopColor={gradientColors[1]} />
70
+ </linearGradient>
71
+ </defs>
72
+ </svg>
73
+
74
+ {/* Center content */}
75
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
76
+ <Icon className={`w-4 h-4 mb-1`} style={{ color: gradientColors[0] }} />
77
+ <motion.span
78
+ className="text-2xl font-bold font-mono text-white"
79
+ initial={{ opacity: 0 }}
80
+ animate={{ opacity: 1 }}
81
+ transition={{ delay: 0.5 }}
82
+ >
83
+ {value}%
84
+ </motion.span>
85
+ <span className="text-[10px] text-gray-400 uppercase tracking-wider mt-0.5">{label}</span>
86
+ </div>
87
+ </motion.div>
88
+ );
89
+}
90
+
91
+export default function SystemStatus({ status }: SystemStatusProps) {
92
+ if (!status) return null;
93
+
94
+ return (
95
+ <div className="flex flex-col items-center gap-6 w-full">
96
+ {/* Main HUD Gauge Cluster */}
97
+ <div className="flex items-center gap-10">
98
+ <GlowingGauge
99
+ value={status.cpuLoad}
100
+ color="#3b82f6"
101
+ icon={Cpu}
102
+ label="CPU"
103
+ gradientColors={status.cpuLoad > 80 ? ["#ef4444", "#f97316"] : ["#3b82f6", "#06b6d4"]}
104
+ />
105
+ <GlowingGauge
106
+ value={status.memoryUsage}
107
+ color="#8b5cf6"
108
+ icon={Zap}
109
+ label="RAM"
110
+ gradientColors={status.memoryUsage > 80 ? ["#eab308", "#f97316"] : ["#8b5cf6", "#ec4899"]}
111
+ />
112
+ </div>
113
+
114
+ {/* Sub-metrics panel with glowing border */}
115
+ <motion.div
116
+ className="relative w-full max-w-md"
117
+ initial={{ opacity: 0, y: 20 }}
118
+ animate={{ opacity: 1, y: 0 }}
119
+ transition={{ delay: 0.3, duration: 0.5 }}
120
+ >
121
+ {/* Glowing border effect */}
122
+ <div className="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/30 via-purple-500/30 to-pink-500/30 rounded-2xl blur-sm" />
123
+
124
+ <div className="relative glass-panel px-6 py-4 rounded-2xl flex items-center justify-between gap-6 text-xs">
125
+ {/* Storage Linear Bar */}
126
+ <div className="flex-1">
127
+ <div className="flex justify-between mb-1.5 text-gray-400">
128
+ <span className="flex items-center gap-1.5">
129
+ <HardDrive className="w-3 h-3 text-emerald-400" />
130
+ <span className="text-emerald-400/80">Storage</span>
131
+ </span>
132
+ <span className="text-emerald-400 font-mono">{status.storageUsage}%</span>
133
+ </div>
134
+ <div className="h-2 bg-white/5 rounded-full overflow-hidden">
135
+ <motion.div
136
+ className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-cyan-500"
137
+ initial={{ width: 0 }}
138
+ animate={{ width: `${status.storageUsage}%` }}
139
+ transition={{ duration: 1, ease: "easeOut" }}
140
+ />
141
+ </div>
142
+ </div>
143
+
144
+ <div className="w-px h-10 bg-gradient-to-b from-transparent via-white/10 to-transparent" />
145
+
146
+ {/* Uptime */}
147
+ <div className="flex flex-col items-center">
148
+ <span className="text-gray-500 uppercase tracking-widest text-[10px] mb-1">Uptime</span>
149
+ <div className="flex items-center gap-1.5">
150
+ <Activity className="w-3 h-3 text-blue-400" />
151
+ <span className="font-mono text-white font-medium">{status.uptime}</span>
152
+ </div>
153
+ </div>
154
+
155
+ <div className="w-px h-10 bg-gradient-to-b from-transparent via-white/10 to-transparent" />
156
+
157
+ {/* Runners */}
158
+ <div className="flex flex-col items-center">
159
+ <span className="text-gray-500 uppercase tracking-widest text-[10px] mb-1">Runners</span>
160
+ <div className="flex items-center gap-2">
161
+ <motion.span
162
+ className="relative flex h-2.5 w-2.5"
163
+ animate={{ scale: [1, 1.2, 1] }}
164
+ transition={{ duration: 2, repeat: Infinity }}
165
+ >
166
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
167
+ <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500" />
168
+ </motion.span>
169
+ <span className="font-mono text-green-400 font-medium">{status.activeRunners} Active</span>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </motion.div>
174
+ </div>
175
+ );
176
+}
0
177
▼src/components/admin/TrendingDevelopers.tsx
+103-0
1
+import { motion } from "framer-motion";
2
+import { TrendingUp, Flame, Star } from "lucide-react";
3
+
4
+interface Developer {
5
+ rank: number;
6
+ name: string;
7
+ lang: string;
8
+ color: string;
9
+}
10
+
11
+interface Props {
12
+ developers: Developer[];
13
+}
14
+
15
+export default function TrendingDevelopers({ developers }: Props) {
16
+ const getRankIcon = (rank: number) => {
17
+ if (rank === 1) return <Flame className="w-4 h-4 text-orange-400" />;
18
+ if (rank === 2) return <Star className="w-4 h-4 text-yellow-400" />;
19
+ if (rank === 3) return <TrendingUp className="w-4 h-4 text-green-400" />;
20
+ return <span className="text-xs font-mono text-gray-500 w-4 text-center">{rank}</span>;
21
+ };
22
+
23
+ const getGradient = (index: number) => {
24
+ const gradients = [
25
+ "from-orange-500 via-amber-500 to-yellow-500",
26
+ "from-purple-500 via-violet-500 to-indigo-500",
27
+ "from-cyan-500 via-blue-500 to-indigo-500",
28
+ "from-green-500 via-emerald-500 to-teal-500",
29
+ "from-pink-500 via-rose-500 to-red-500",
30
+ ];
31
+ return gradients[index % gradients.length];
32
+ };
33
+
34
+ return (
35
+ <div className="p-6">
36
+ <motion.div
37
+ initial={{ opacity: 0 }}
38
+ animate={{ opacity: 1 }}
39
+ className="flex items-center gap-2 mb-6"
40
+ >
41
+ <Flame className="w-4 h-4 text-rose-400" />
42
+ <h3 className="text-sm uppercase tracking-widest bg-gradient-to-r from-rose-400 to-orange-400 bg-clip-text text-transparent font-semibold">
43
+ Trending Developers
44
+ </h3>
45
+ </motion.div>
46
+
47
+ <div className="space-y-3">
48
+ {developers.map((dev, i) => (
49
+ <motion.div
50
+ key={dev.name}
51
+ initial={{ opacity: 0, x: -20 }}
52
+ animate={{ opacity: 1, x: 0 }}
53
+ transition={{ delay: 0.2 + i * 0.08, type: "spring", stiffness: 100 }}
54
+ className="relative group cursor-pointer"
55
+ >
56
+ {/* Hover glow effect */}
57
+ <div className={`absolute -inset-1 bg-gradient-to-r ${getGradient(i)} rounded-lg opacity-0 group-hover:opacity-20 blur-md transition-opacity duration-300`} />
58
+
59
+ <div className="relative flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition-colors">
60
+ {/* Rank */}
61
+ <div className="w-6 flex justify-center">
62
+ {getRankIcon(dev.rank)}
63
+ </div>
64
+
65
+ {/* Avatar with gradient border */}
66
+ <div className={`relative h-10 w-10 rounded-full p-[2px] bg-gradient-to-br ${getGradient(i)}`}>
67
+ <div className="h-full w-full rounded-full bg-[#0a0a15] flex items-center justify-center">
68
+ <span className="text-sm font-bold text-white">
69
+ {dev.name[0].toUpperCase()}
70
+ </span>
71
+ </div>
72
+ </div>
73
+
74
+ {/* Info */}
75
+ <div className="flex-1 min-w-0">
76
+ <div className="text-sm text-gray-200 font-medium group-hover:text-white transition-colors truncate">
77
+ {dev.name}
78
+ </div>
79
+ <div className="flex items-center gap-2 mt-1">
80
+ {/* Progress bar */}
81
+ <div className="h-1.5 w-12 bg-gray-800 rounded-full overflow-hidden">
82
+ <motion.div
83
+ className={`h-full rounded-full bg-gradient-to-r ${getGradient(i)}`}
84
+ initial={{ width: 0 }}
85
+ animate={{ width: `${70 + Math.random() * 30}%` }}
86
+ transition={{ delay: 0.5 + i * 0.1, duration: 0.8 }}
87
+ />
88
+ </div>
89
+ <span
90
+ className="text-[10px] font-medium"
91
+ style={{ color: dev.color }}
92
+ >
93
+ {dev.lang}
94
+ </span>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </motion.div>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ );
103
+}
0
104
▼src/components/dashboard/ActivityStream.tsx
+113-0
1
+
2
+import * as React from "react";
3
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
4
+import { cn } from "@/lib/utils";
5
+
6
+interface ActivityUser {
7
+ id: string;
8
+ username: string;
9
+ avatarUrl?: string;
10
+}
11
+
12
+interface ActivityStreamProps {
13
+ activeCount: number;
14
+ users: ActivityUser[];
15
+ className?: string;
16
+}
17
+
18
+export function ActivityStream({ activeCount, users, className }: ActivityStreamProps) {
19
+ // Mock timeline dots for visual effect matching the image
20
+ // In a real app, these could represent specific timestamps of events
21
+ // Professional, minimal palette (monochromatic with accents)
22
+ const dots = [
23
+ { color: "bg-indigo-500", left: "10%", delay: "0s" },
24
+ { color: "bg-indigo-400", left: "30%", delay: "1s" },
25
+ { color: "bg-violet-500", left: "50%", delay: "2s" },
26
+ { color: "bg-indigo-300", left: "65%", delay: "1.5s" },
27
+ { color: "bg-white", left: "85%", delay: "0.5s" },
28
+ ];
29
+
30
+ return (
31
+ <div className={cn("w-full relative overflow-hidden rounded-xl border border-white/10 bg-white/5 p-6 backdrop-blur-xl transition-all hover:bg-white/10 hover:border-white/20", className)}>
32
+ {/* Subtle background glow effect using CSS gradients */}
33
+ <div className="absolute -top-24 -right-24 w-48 h-48 bg-indigo-500/20 rounded-full blur-3xl pointer-events-none" />
34
+ <div className="absolute -bottom-24 -left-24 w-48 h-48 bg-purple-500/10 rounded-full blur-3xl pointer-events-none" />
35
+
36
+ <div className="flex flex-col space-y-6 relative z-10">
37
+ <div className="flex items-center justify-between">
38
+ <h3 className="text-[10px] font-bold tracking-[0.2em] text-muted-foreground/70 uppercase">
39
+ Live Code Review Activity
40
+ </h3>
41
+ <div className="flex items-center gap-2">
42
+ <span className="relative flex h-2 w-2">
43
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
44
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
45
+ </span>
46
+ <span className="text-[10px] font-medium text-muted-foreground/70 uppercase tracking-wider">Real-time</span>
47
+ </div>
48
+ </div>
49
+
50
+ <div className="flex items-center gap-12">
51
+ {/* Left Stat - Clean & Big */}
52
+ <div className="flex flex-col">
53
+ <span className="text-5xl font-bold tracking-tighter text-transparent bg-clip-text bg-gradient-to-br from-white to-white/50">
54
+ {activeCount}
55
+ </span>
56
+ <span className="text-xs font-medium text-muted-foreground mt-1">
57
+ Developers shipping code
58
+ </span>
59
+ </div>
60
+
61
+ {/* Timeline Visualization - Minimal & Tech */}
62
+ <div className="flex-1 relative h-px bg-gradient-to-r from-transparent via-white/20 to-transparent flex items-center">
63
+ {/* Moving light effect */}
64
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-indigo-500/50 to-transparent w-1/2 blur-[1px] animate-[shimmer_3s_infinite_linear]" />
65
+
66
+ {/* Dots with pulse */}
67
+ {dots.map((dot, i) => (
68
+ <div
69
+ key={i}
70
+ className={cn("absolute w-1.5 h-1.5 rounded-full shadow-[0_0_10px_rgba(99,102,241,0.5)] transition-all duration-1000", dot.color)}
71
+ style={{ left: dot.left }}
72
+ >
73
+ <div className={cn("absolute inset-0 rounded-full animate-ping opacity-20", dot.color)} style={{ animationDelay: dot.delay }} />
74
+ </div>
75
+ ))}
76
+ </div>
77
+
78
+ {/* User Avatars - Tighter & Cleaner */}
79
+ <div className="flex items-center -space-x-4 pl-4">
80
+ {users.slice(0, 5).map((user, i) => (
81
+ <div key={user.id} className="relative group transition-transform hover:-translate-y-1 hover:z-20">
82
+ <Avatar className="w-12 h-12 border-2 border-[#09090b] ring-1 ring-white/10 transition-shadow group-hover:ring-indigo-500/50">
83
+ <AvatarImage src={user.avatarUrl} alt={user.username} className="object-cover" />
84
+ <AvatarFallback className="text-xs bg-zinc-900 font-medium text-zinc-400">
85
+ {user.username.slice(0, 2).toUpperCase()}
86
+ </AvatarFallback>
87
+ </Avatar>
88
+ {/* Tooltip on hover */}
89
+ <div className="absolute -bottom-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity bg-black/90 text-white text-[10px] px-2 py-1 rounded whitespace-nowrap pointer-events-none border border-white/10">
90
+ @{user.username}
91
+ </div>
92
+ </div>
93
+ ))}
94
+ {users.length > 5 && (
95
+ <div className="relative z-0 group">
96
+ <div className="w-12 h-12 rounded-full bg-[#09090b] border-2 border-[#09090b] ring-1 ring-white/10 flex items-center justify-center text-xs font-medium text-zinc-400 group-hover:border-indigo-500/50 transition-colors">
97
+ +{users.length - 5}
98
+ </div>
99
+ </div>
100
+ )}
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <style>{`
106
+ @keyframes shimmer {
107
+ 0% { transform: translateX(-100%); }
108
+ 100% { transform: translateX(200%); }
109
+ }
110
+ `}</style>
111
+ </div>
112
+ );
113
+}
0
114
▼src/components/home/FeaturesSection.tsx
+145-0
1
+"use client";
2
+import { HoverEffect } from "@/components/ui/card-hover-effect";
3
+import { motion } from "framer-motion";
4
+import {
5
+ Code,
6
+ GitBranch,
7
+ Activity,
8
+ Users,
9
+ BookOpen,
10
+ Settings,
11
+ Layers,
12
+ Bot,
13
+ GitMerge,
14
+ BarChart3,
15
+ MessageSquare,
16
+ Terminal,
17
+} from "lucide-react";
18
+
19
+const graphiteFeatures = [
20
+ {
21
+ title: "Stacked Pull Requests",
22
+ description:
23
+ "Break large changes into reviewable stacks. Each PR builds on the previous, enabling faster feedback cycles.",
24
+ icon: <Layers className="h-6 w-6 text-primary" />,
25
+ link: "/inbox",
26
+ },
27
+ {
28
+ title: "AI Code Review",
29
+ description:
30
+ "Get instant feedback from AI reviewers. Catch bugs, suggest improvements, and enforce best practices automatically.",
31
+ icon: <Bot className="h-6 w-6 text-purple-400" />,
32
+ },
33
+ {
34
+ title: "Smart Merge Queue",
35
+ description:
36
+ "Stack-aware merge queue with automatic rebasing. Never deal with merge conflicts again.",
37
+ icon: <GitMerge className="h-6 w-6 text-green-400" />,
38
+ },
39
+ {
40
+ title: "Developer Metrics",
41
+ description:
42
+ "Track PR velocity, review efficiency, and team performance with actionable insights.",
43
+ icon: <BarChart3 className="h-6 w-6 text-orange-400" />,
44
+ },
45
+ {
46
+ title: "Slack Notifications",
47
+ description:
48
+ "Get actionable PR notifications in Slack. Review, approve, and merge without leaving chat.",
49
+ icon: <MessageSquare className="h-6 w-6 text-blue-400" />,
50
+ },
51
+ {
52
+ title: "och CLI",
53
+ description:
54
+ "Powerful CLI for managing stacks. Create, sync, and submit PRs from your terminal.",
55
+ icon: <Terminal className="h-6 w-6 text-cyan-400" />,
56
+ },
57
+];
58
+
59
+const coreFeatures = [
60
+ {
61
+ title: "Git Hosting",
62
+ description:
63
+ "Full Git support with SSH and HTTP protocols. Manage branches, tags, and commits with ease.",
64
+ icon: <Code className="h-6 w-6 text-primary" />,
65
+ },
66
+ {
67
+ title: "Pull Requests",
68
+ description:
69
+ "Review code changes with inline comments, approvals, and merge strategies.",
70
+ icon: <GitBranch className="h-6 w-6 text-primary" />,
71
+ },
72
+ {
73
+ title: "CI/CD Pipelines",
74
+ description:
75
+ "GitHub Actions compatible workflows. Build, test, and deploy automatically.",
76
+ icon: <Activity className="h-6 w-6 text-primary" />,
77
+ },
78
+ {
79
+ title: "Team Collaboration",
80
+ description:
81
+ "Collaborators, teams, and fine-grained permissions for enterprise workflows.",
82
+ icon: <Users className="h-6 w-6 text-primary" />,
83
+ },
84
+ {
85
+ title: "Documentation",
86
+ description:
87
+ "Built-in wiki, README rendering, and project documentation support.",
88
+ icon: <BookOpen className="h-6 w-6 text-primary" />,
89
+ },
90
+ {
91
+ title: "Administration",
92
+ description:
93
+ "Advanced controls, audit logs, and monitoring for system administrators.",
94
+ icon: <Settings className="h-6 w-6 text-primary" />,
95
+ },
96
+];
97
+
98
+export default function FeaturesSection() {
99
+ return (
100
+ <div className="py-20 relative">
101
+ {/* Graphite Features */}
102
+ <div className="max-w-7xl mx-auto px-4 mb-20">
103
+ <motion.div
104
+ initial={{ opacity: 0, y: 20 }}
105
+ whileInView={{ opacity: 1, y: 0 }}
106
+ transition={{ duration: 0.5 }}
107
+ viewport={{ once: true }}
108
+ className="text-center mb-10"
109
+ >
110
+ <span className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-primary/20 bg-primary/5 text-sm text-primary mb-4">
111
+ ✨ New Features
112
+ </span>
113
+ <h2 className="text-3xl md:text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-b from-neutral-50 to-neutral-400">
114
+ Graphite-Inspired Developer Experience
115
+ </h2>
116
+ <p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
117
+ Stack your PRs, automate reviews, and ship faster with our modern code review workflow
118
+ </p>
119
+ </motion.div>
120
+
121
+ <HoverEffect items={graphiteFeatures} />
122
+ </div>
123
+
124
+ {/* Core Features */}
125
+ <div className="max-w-7xl mx-auto px-4 border-t border-white/5 pt-20">
126
+ <motion.div
127
+ initial={{ opacity: 0, y: 20 }}
128
+ whileInView={{ opacity: 1, y: 0 }}
129
+ transition={{ duration: 0.5 }}
130
+ viewport={{ once: true }}
131
+ className="text-center mb-10"
132
+ >
133
+ <h2 className="text-2xl md:text-3xl font-bold text-foreground">
134
+ Everything you need to ship code
135
+ </h2>
136
+ <p className="mt-4 text-muted-foreground">
137
+ A complete platform for version control, code review, and continuous integration
138
+ </p>
139
+ </motion.div>
140
+
141
+ <HoverEffect items={coreFeatures} />
142
+ </div>
143
+ </div>
144
+ );
145
+}
0
146
▼src/components/home/HeroSection.tsx
+120-0
1
+"use client";
2
+import { FlipWords } from "@/components/ui/flip-words";
3
+import { Spotlight } from "@/components/ui/spotlight";
4
+import { ColourfulText } from "@/components/ui/colourful-text";
5
+import { motion } from "framer-motion";
6
+import { ArrowRight, Github, Star, GitBranch, Zap } from "lucide-react";
7
+
8
+export default function HeroSection() {
9
+ const words = ["Teams", "Developers", "Enterprises", "Startups"];
10
+
11
+ return (
12
+ <div className="h-[40rem] w-full rounded-md flex md:items-center md:justify-center bg-background/[0.96] antialiased bg-grid-white/[0.02] relative overflow-hidden">
13
+ {/* Spotlight effect */}
14
+ <Spotlight
15
+ className="-top-40 left-0 md:left-60 md:-top-20"
16
+ fill="hsl(var(--primary))"
17
+ />
18
+
19
+ {/* Ambient glow effects */}
20
+ <div className="absolute top-0 left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-[120px] pointer-events-none" />
21
+ <div className="absolute bottom-0 right-1/4 w-80 h-80 bg-purple-500/10 rounded-full blur-[100px] pointer-events-none" />
22
+
23
+ <div className="p-4 max-w-7xl mx-auto relative z-10 w-full pt-20 md:pt-0">
24
+ {/* Badge */}
25
+ <motion.div
26
+ initial={{ opacity: 0, y: 20 }}
27
+ animate={{ opacity: 1, y: 0 }}
28
+ transition={{ duration: 0.5 }}
29
+ className="flex justify-center mb-6"
30
+ >
31
+ <span className="inline-flex items-center gap-2 px-4 py-2 rounded-full border border-primary/20 bg-primary/5 text-sm text-primary">
32
+ <Star className="h-4 w-4 fill-primary" />
33
+ Open Source & Self-Hosted
34
+ </span>
35
+ </motion.div>
36
+
37
+ {/* Main heading with animated blue gradient */}
38
+ <motion.h1
39
+ initial={{ opacity: 0, y: 20 }}
40
+ animate={{ opacity: 1, y: 0 }}
41
+ transition={{ duration: 0.5, delay: 0.1 }}
42
+ className="text-4xl md:text-7xl font-bold text-center"
43
+ >
44
+ <span className="bg-clip-text text-transparent bg-gradient-to-b from-neutral-50 to-neutral-400">
45
+ Self-hosted{" "}
46
+ </span>
47
+ <span className="bg-clip-text text-transparent bg-gradient-to-r from-blue-400 via-cyan-400 to-blue-600 bg-[length:200%_auto] animate-gradient-x">
48
+ Git Platform
49
+ </span>
50
+ <br />
51
+ <span className="text-3xl md:text-6xl bg-clip-text text-transparent bg-gradient-to-b from-neutral-50 to-neutral-400">for Modern{" "}</span>
52
+ <FlipWords words={words} className="text-3xl md:text-6xl" />
53
+ </motion.h1>
54
+
55
+ {/* Description */}
56
+ <motion.p
57
+ initial={{ opacity: 0, y: 20 }}
58
+ animate={{ opacity: 1, y: 0 }}
59
+ transition={{ duration: 0.5, delay: 0.2 }}
60
+ className="mt-6 font-normal text-base text-neutral-300 max-w-xl text-center mx-auto"
61
+ >
62
+ Host your code, manage pull requests, run CI/CD pipelines, and collaborate securely.
63
+ GitHub-compatible, fully self-hosted, and open source.
64
+ </motion.p>
65
+
66
+ {/* CTA Buttons */}
67
+ <motion.div
68
+ initial={{ opacity: 0, y: 20 }}
69
+ animate={{ opacity: 1, y: 0 }}
70
+ transition={{ duration: 0.5, delay: 0.3 }}
71
+ className="flex flex-col sm:flex-row gap-4 justify-center mt-10"
72
+ >
73
+ <a
74
+ href="/register"
75
+ className="group relative inline-flex items-center justify-center gap-2 rounded-lg bg-primary px-8 py-3.5 text-sm font-semibold text-primary-foreground transition-all hover:bg-primary/90 hover:scale-105"
76
+ >
77
+ Get Started
78
+ <ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
79
+
80
+ {/* Glow effect */}
81
+ <span className="absolute inset-0 rounded-lg bg-primary blur-xl opacity-40 group-hover:opacity-60 transition-opacity" />
82
+ </a>
83
+ <a
84
+ href="/explore"
85
+ className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/10 bg-white/5 px-8 py-3.5 text-sm font-medium text-foreground backdrop-blur-sm hover:bg-white/10 hover:border-white/20 transition-all"
86
+ >
87
+ <Github className="h-4 w-4" />
88
+ Explore Repositories
89
+ </a>
90
+ </motion.div>
91
+
92
+ {/* Stats */}
93
+ <motion.div
94
+ initial={{ opacity: 0, y: 20 }}
95
+ animate={{ opacity: 1, y: 0 }}
96
+ transition={{ duration: 0.5, delay: 0.4 }}
97
+ className="flex items-center justify-center gap-8 mt-16"
98
+ >
99
+ <div className="flex items-center gap-2 text-muted-foreground">
100
+ <GitBranch className="h-5 w-5 text-primary" />
101
+ <span className="text-2xl font-bold text-foreground">100%</span>
102
+ <span className="text-sm">Open Source</span>
103
+ </div>
104
+ <div className="w-px h-8 bg-white/10" />
105
+ <div className="flex items-center gap-2 text-muted-foreground">
106
+ <Zap className="h-5 w-5 text-yellow-500" />
107
+ <span className="text-2xl font-bold text-foreground">Graphite</span>
108
+ <span className="text-sm">Features</span>
109
+ </div>
110
+ <div className="w-px h-8 bg-white/10" />
111
+ <div className="flex items-center gap-2 text-muted-foreground">
112
+ <Star className="h-5 w-5 text-orange-500" />
113
+ <span className="text-2xl font-bold text-foreground">AI</span>
114
+ <span className="text-sm">Powered Reviews</span>
115
+ </div>
116
+ </motion.div>
117
+ </div>
118
+ </div>
119
+ );
120
+}
0
121
▼src/components/issues/IssueDetail.tsx
+347-0
1
+"use client";
2
+import { motion } from "framer-motion";
3
+import {
4
+ AlertCircle,
5
+ CheckCircle2,
6
+ MessageSquare,
7
+ User,
8
+ Tag,
9
+ Calendar,
10
+ Clock,
11
+ Link2,
12
+ GitBranch,
13
+ MoreHorizontal,
14
+ Edit3,
15
+ Pin,
16
+ Lock,
17
+ Trash2
18
+} from "lucide-react";
19
+import { useState } from "react";
20
+
21
+interface Issue {
22
+ id: string;
23
+ number: number;
24
+ title: string;
25
+ body: string;
26
+ state: "open" | "closed";
27
+ createdAt: string;
28
+ author: {
29
+ username: string;
30
+ avatarUrl?: string;
31
+ };
32
+ assignees?: Array<{
33
+ username: string;
34
+ avatarUrl?: string;
35
+ }>;
36
+ labels?: Array<{
37
+ name: string;
38
+ color: string;
39
+ }>;
40
+}
41
+
42
+interface Props {
43
+ issue: Issue;
44
+ bodyHtml: string;
45
+ repoOwner: string;
46
+ repoName: string;
47
+}
48
+
49
+function timeAgo(date: string): string {
50
+ const now = new Date();
51
+ const past = new Date(date);
52
+ const diffMs = now.getTime() - past.getTime();
53
+ const diffMins = Math.floor(diffMs / 60000);
54
+
55
+ if (diffMins < 1) return 'just now';
56
+ if (diffMins < 60) return `${diffMins} minutes ago`;
57
+
58
+ const diffHours = Math.floor(diffMins / 60);
59
+ if (diffHours < 24) return `${diffHours} hours ago`;
60
+
61
+ const diffDays = Math.floor(diffHours / 24);
62
+ if (diffDays < 30) return `${diffDays} days ago`;
63
+
64
+ const diffMonths = Math.floor(diffDays / 30);
65
+ return `${diffMonths} months ago`;
66
+}
67
+
68
+export default function IssueDetail({ issue, bodyHtml, repoOwner, repoName }: Props) {
69
+ const [showMenu, setShowMenu] = useState(false);
70
+
71
+ return (
72
+ <div className="max-w-6xl mx-auto">
73
+ {/* Title Section */}
74
+ <motion.div
75
+ initial={{ opacity: 0, y: -20 }}
76
+ animate={{ opacity: 1, y: 0 }}
77
+ className="mb-8 pb-6 border-b border-white/10"
78
+ >
79
+ <div className="flex items-start justify-between gap-4 mb-4">
80
+ <div className="flex items-center gap-3">
81
+ <h1 className="text-2xl md:text-3xl font-bold text-white">
82
+ {issue.title}
83
+ </h1>
84
+ <span className="text-2xl md:text-3xl text-gray-500 font-light">
85
+ #{issue.number}
86
+ </span>
87
+ </div>
88
+
89
+ {/* Edit Button */}
90
+ <motion.button
91
+ whileHover={{ scale: 1.05 }}
92
+ whileTap={{ scale: 0.95 }}
93
+ className="flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 transition-all"
94
+ >
95
+ <Edit3 className="h-4 w-4" />
96
+ Edit
97
+ </motion.button>
98
+ </div>
99
+
100
+ <div className="flex items-center gap-3 text-sm">
101
+ {/* Status Badge */}
102
+ <motion.div
103
+ initial={{ scale: 0.9 }}
104
+ animate={{ scale: 1 }}
105
+ className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full font-medium ${issue.state === "open"
106
+ ? "bg-green-500/20 text-green-400 border border-green-500/30"
107
+ : "bg-purple-500/20 text-purple-400 border border-purple-500/30"
108
+ }`}
109
+ >
110
+ {issue.state === "open" ? (
111
+ <AlertCircle className="h-4 w-4" />
112
+ ) : (
113
+ <CheckCircle2 className="h-4 w-4" />
114
+ )}
115
+ {issue.state === "open" ? "Open" : "Closed"}
116
+ </motion.div>
117
+
118
+ <span className="text-gray-400">
119
+ <span className="font-medium text-white hover:text-cyan-400 cursor-pointer transition-colors">
120
+ {issue.author.username}
121
+ </span>
122
+ {" "}opened this issue {timeAgo(issue.createdAt)}
123
+ </span>
124
+ </div>
125
+ </motion.div>
126
+
127
+ {/* Main Content Grid */}
128
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-8">
129
+ {/* Left Column - Issue Body & Comments */}
130
+ <div className="space-y-6">
131
+ {/* Issue Body Card */}
132
+ <motion.div
133
+ initial={{ opacity: 0, y: 20 }}
134
+ animate={{ opacity: 1, y: 0 }}
135
+ transition={{ delay: 0.1 }}
136
+ className="relative"
137
+ >
138
+ {/* Gradient border */}
139
+ <div className="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/20 via-transparent to-purple-500/20 rounded-xl opacity-50" />
140
+
141
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm overflow-hidden">
142
+ {/* Author Header */}
143
+ <div className="flex items-center justify-between px-4 py-3 bg-white/[0.02] border-b border-white/5">
144
+ <div className="flex items-center gap-3">
145
+ {issue.author.avatarUrl ? (
146
+ <img
147
+ src={issue.author.avatarUrl}
148
+ alt=""
149
+ className="h-8 w-8 rounded-full ring-2 ring-white/10"
150
+ />
151
+ ) : (
152
+ <div className="h-8 w-8 rounded-full bg-gradient-to-br from-cyan-400 to-blue-600 flex items-center justify-center text-sm font-bold text-white ring-2 ring-white/10">
153
+ {issue.author.username[0].toUpperCase()}
154
+ </div>
155
+ )}
156
+ <div>
157
+ <span className="font-medium text-white text-sm">
158
+ {issue.author.username}
159
+ </span>
160
+ <span className="text-xs text-gray-500 ml-2">
161
+ commented {timeAgo(issue.createdAt)}
162
+ </span>
163
+ </div>
164
+ </div>
165
+
166
+ {/* Actions Menu */}
167
+ <div className="relative">
168
+ <button
169
+ onClick={() => setShowMenu(!showMenu)}
170
+ className="p-1.5 rounded-lg hover:bg-white/10 transition-colors"
171
+ >
172
+ <MoreHorizontal className="h-4 w-4 text-gray-500" />
173
+ </button>
174
+
175
+ {showMenu && (
176
+ <motion.div
177
+ initial={{ opacity: 0, scale: 0.95 }}
178
+ animate={{ opacity: 1, scale: 1 }}
179
+ className="absolute right-0 top-full mt-1 w-48 rounded-lg border border-white/10 bg-[#161b22] shadow-xl z-10"
180
+ >
181
+ <div className="p-1">
182
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-white/5 rounded-md transition-colors">
183
+ <Link2 className="h-4 w-4" />
184
+ Copy link
185
+ </button>
186
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-white/5 rounded-md transition-colors">
187
+ <Pin className="h-4 w-4" />
188
+ Pin issue
189
+ </button>
190
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-white/5 rounded-md transition-colors">
191
+ <Lock className="h-4 w-4" />
192
+ Lock conversation
193
+ </button>
194
+ <div className="border-t border-white/5 my-1" />
195
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-md transition-colors">
196
+ <Trash2 className="h-4 w-4" />
197
+ Delete issue
198
+ </button>
199
+ </div>
200
+ </motion.div>
201
+ )}
202
+ </div>
203
+ </div>
204
+
205
+ {/* Body Content */}
206
+ <div
207
+ className="p-5 prose prose-invert prose-sm max-w-none prose-p:text-gray-300 prose-headings:text-white prose-a:text-cyan-400 prose-code:text-pink-400 prose-pre:bg-black/30"
208
+ dangerouslySetInnerHTML={{ __html: bodyHtml }}
209
+ />
210
+ </div>
211
+ </motion.div>
212
+
213
+ {/* Add Comment Section */}
214
+ <motion.div
215
+ initial={{ opacity: 0, y: 20 }}
216
+ animate={{ opacity: 1, y: 0 }}
217
+ transition={{ delay: 0.2 }}
218
+ className="relative"
219
+ >
220
+ <div className="rounded-xl border border-white/10 bg-[#0d1117]/80 p-4">
221
+ <textarea
222
+ placeholder="Leave a comment..."
223
+ className="w-full h-24 bg-transparent border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 resize-none"
224
+ />
225
+ <div className="flex justify-end mt-3">
226
+ <motion.button
227
+ whileHover={{ scale: 1.02 }}
228
+ whileTap={{ scale: 0.98 }}
229
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-white shadow-lg shadow-green-500/20"
230
+ >
231
+ <MessageSquare className="h-4 w-4" />
232
+ Comment
233
+ </motion.button>
234
+ </div>
235
+ </div>
236
+ </motion.div>
237
+ </div>
238
+
239
+ {/* Right Sidebar */}
240
+ <motion.div
241
+ initial={{ opacity: 0, x: 20 }}
242
+ animate={{ opacity: 1, x: 0 }}
243
+ transition={{ delay: 0.15 }}
244
+ className="space-y-6"
245
+ >
246
+ {/* Assignees */}
247
+ <div className="relative">
248
+ <div className="absolute -inset-[1px] bg-gradient-to-b from-cyan-500/10 to-transparent rounded-xl opacity-50" />
249
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/60 p-4">
250
+ <div className="flex items-center justify-between mb-3">
251
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2">
252
+ <User className="h-4 w-4 text-cyan-400" />
253
+ Assignees
254
+ </h3>
255
+ <button className="text-xs text-gray-500 hover:text-cyan-400 transition-colors">
256
+ Edit
257
+ </button>
258
+ </div>
259
+ {issue.assignees && issue.assignees.length > 0 ? (
260
+ <div className="space-y-2">
261
+ {issue.assignees.map(assignee => (
262
+ <div key={assignee.username} className="flex items-center gap-2">
263
+ <div className="h-6 w-6 rounded-full bg-gradient-to-br from-purple-400 to-pink-500 flex items-center justify-center text-xs font-bold text-white">
264
+ {assignee.username[0].toUpperCase()}
265
+ </div>
266
+ <span className="text-sm text-gray-300">{assignee.username}</span>
267
+ </div>
268
+ ))}
269
+ </div>
270
+ ) : (
271
+ <p className="text-sm text-gray-500">No one assigned</p>
272
+ )}
273
+ </div>
274
+ </div>
275
+
276
+ {/* Labels */}
277
+ <div className="relative">
278
+ <div className="absolute -inset-[1px] bg-gradient-to-b from-purple-500/10 to-transparent rounded-xl opacity-50" />
279
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/60 p-4">
280
+ <div className="flex items-center justify-between mb-3">
281
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2">
282
+ <Tag className="h-4 w-4 text-purple-400" />
283
+ Labels
284
+ </h3>
285
+ <button className="text-xs text-gray-500 hover:text-purple-400 transition-colors">
286
+ Edit
287
+ </button>
288
+ </div>
289
+ {issue.labels && issue.labels.length > 0 ? (
290
+ <div className="flex flex-wrap gap-2">
291
+ {issue.labels.map(label => (
292
+ <span
293
+ key={label.name}
294
+ className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium"
295
+ style={{
296
+ backgroundColor: `${label.color}20`,
297
+ color: label.color,
298
+ border: `1px solid ${label.color}40`
299
+ }}
300
+ >
301
+ {label.name}
302
+ </span>
303
+ ))}
304
+ </div>
305
+ ) : (
306
+ <p className="text-sm text-gray-500">None yet</p>
307
+ )}
308
+ </div>
309
+ </div>
310
+
311
+ {/* Development */}
312
+ <div className="relative">
313
+ <div className="absolute -inset-[1px] bg-gradient-to-b from-orange-500/10 to-transparent rounded-xl opacity-50" />
314
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/60 p-4">
315
+ <div className="flex items-center justify-between mb-3">
316
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2">
317
+ <GitBranch className="h-4 w-4 text-orange-400" />
318
+ Development
319
+ </h3>
320
+ </div>
321
+ <p className="text-sm text-gray-500">
322
+ No branches or pull requests linked
323
+ </p>
324
+ </div>
325
+ </div>
326
+
327
+ {/* Timeline Info */}
328
+ <div className="relative">
329
+ <div className="absolute -inset-[1px] bg-gradient-to-b from-green-500/10 to-transparent rounded-xl opacity-50" />
330
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/60 p-4">
331
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-3">
332
+ <Clock className="h-4 w-4 text-green-400" />
333
+ Timeline
334
+ </h3>
335
+ <div className="space-y-2 text-sm text-gray-500">
336
+ <div className="flex items-center gap-2">
337
+ <Calendar className="h-3.5 w-3.5" />
338
+ <span>Created {timeAgo(issue.createdAt)}</span>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </motion.div>
344
+ </div>
345
+ </div>
346
+ );
347
+}
0
348
▼src/components/issues/IssuesList.tsx
+299-0
1
+"use client";
2
+import { motion, AnimatePresence } from "framer-motion";
3
+import {
4
+ AlertCircle,
5
+ CheckCircle2,
6
+ MessageSquare,
7
+ Search,
8
+ Tag,
9
+ Calendar,
10
+ User,
11
+ ArrowUpDown,
12
+ Plus,
13
+ Sparkles,
14
+ Filter
15
+} from "lucide-react";
16
+import { useState } from "react";
17
+
18
+interface Issue {
19
+ id: string;
20
+ number: number;
21
+ title: string;
22
+ state: "open" | "closed";
23
+ createdAt: string;
24
+ commentCount: number;
25
+ author: {
26
+ username: string;
27
+ avatarUrl?: string;
28
+ };
29
+ labels?: Array<{
30
+ name: string;
31
+ color: string;
32
+ }>;
33
+}
34
+
35
+interface Props {
36
+ issues: Issue[];
37
+ openCount: number;
38
+ closedCount: number;
39
+ repoOwner: string;
40
+ repoName: string;
41
+}
42
+
43
+function timeAgo(date: string): string {
44
+ const now = new Date();
45
+ const past = new Date(date);
46
+ const diffMs = now.getTime() - past.getTime();
47
+ const diffMins = Math.floor(diffMs / 60000);
48
+
49
+ if (diffMins < 1) return 'just now';
50
+ if (diffMins < 60) return `${diffMins} minutes ago`;
51
+
52
+ const diffHours = Math.floor(diffMins / 60);
53
+ if (diffHours < 24) return `${diffHours} hours ago`;
54
+
55
+ const diffDays = Math.floor(diffHours / 24);
56
+ if (diffDays < 30) return `${diffDays} days ago`;
57
+
58
+ const diffMonths = Math.floor(diffDays / 30);
59
+ return `${diffMonths} months ago`;
60
+}
61
+
62
+export default function IssuesList({ issues, openCount, closedCount, repoOwner, repoName }: Props) {
63
+ const [filter, setFilter] = useState<"open" | "closed" | "all">("open");
64
+ const [searchQuery, setSearchQuery] = useState("");
65
+
66
+ const filteredIssues = issues.filter(issue => {
67
+ const matchesFilter = filter === "all" || issue.state === filter;
68
+ const matchesSearch = issue.title.toLowerCase().includes(searchQuery.toLowerCase());
69
+ return matchesFilter && matchesSearch;
70
+ });
71
+
72
+ return (
73
+ <div className="space-y-6">
74
+ {/* Enhanced Header with Glow */}
75
+ <motion.div
76
+ initial={{ opacity: 0, y: -20 }}
77
+ animate={{ opacity: 1, y: 0 }}
78
+ className="flex flex-col lg:flex-row lg:items-center justify-between gap-4"
79
+ >
80
+ {/* Search Input with Glass Effect */}
81
+ <div className="relative flex-1 max-w-xl group">
82
+ <div className="absolute -inset-0.5 bg-gradient-to-r from-cyan-500/20 via-purple-500/20 to-pink-500/20 rounded-xl blur opacity-0 group-focus-within:opacity-100 transition-opacity" />
83
+ <div className="relative">
84
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" />
85
+ <input
86
+ type="search"
87
+ placeholder="Search all issues..."
88
+ value={searchQuery}
89
+ onChange={(e) => setSearchQuery(e.target.value)}
90
+ className="w-full rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm pl-10 pr-4 py-2.5 text-sm text-white placeholder:text-gray-500 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ {/* Action Buttons */}
96
+ <div className="flex gap-3">
97
+ <motion.a
98
+ href={`/${repoOwner}/${repoName}/labels`}
99
+ whileHover={{ scale: 1.02 }}
100
+ whileTap={{ scale: 0.98 }}
101
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 hover:border-white/20 transition-all"
102
+ >
103
+ <Tag className="h-4 w-4" />
104
+ Labels
105
+ </motion.a>
106
+ <motion.a
107
+ href={`/${repoOwner}/${repoName}/milestones`}
108
+ whileHover={{ scale: 1.02 }}
109
+ whileTap={{ scale: 0.98 }}
110
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 bg-white/5 text-sm text-gray-300 hover:bg-white/10 hover:border-white/20 transition-all"
111
+ >
112
+ <Calendar className="h-4 w-4" />
113
+ Milestones
114
+ </motion.a>
115
+ <motion.a
116
+ href={`/${repoOwner}/${repoName}/issues/new`}
117
+ whileHover={{ scale: 1.05 }}
118
+ whileTap={{ scale: 0.95 }}
119
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gradient-to-r from-green-500 to-emerald-600 text-sm font-medium text-white shadow-lg shadow-green-500/25 hover:shadow-green-500/40 transition-all"
120
+ >
121
+ <Plus className="h-4 w-4" />
122
+ New Issue
123
+ </motion.a>
124
+ </div>
125
+ </motion.div>
126
+
127
+ {/* Issues Container with Glass Effect */}
128
+ <motion.div
129
+ initial={{ opacity: 0 }}
130
+ animate={{ opacity: 1 }}
131
+ transition={{ delay: 0.1 }}
132
+ className="relative"
133
+ >
134
+ {/* Gradient border effect */}
135
+ <div className="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/30 via-purple-500/20 to-pink-500/30 rounded-xl blur-sm opacity-50" />
136
+
137
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm overflow-hidden">
138
+ {/* Filter Header */}
139
+ <div className="flex items-center justify-between gap-4 border-b border-white/5 bg-white/[0.02] px-4 py-3">
140
+ <div className="flex items-center gap-1">
141
+ <button
142
+ onClick={() => setFilter("open")}
143
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "open"
144
+ ? "bg-green-500/10 text-green-400 border border-green-500/30"
145
+ : "text-gray-400 hover:text-white hover:bg-white/5"
146
+ }`}
147
+ >
148
+ <AlertCircle className="h-4 w-4" />
149
+ {openCount} Open
150
+ </button>
151
+ <button
152
+ onClick={() => setFilter("closed")}
153
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${filter === "closed"
154
+ ? "bg-purple-500/10 text-purple-400 border border-purple-500/30"
155
+ : "text-gray-400 hover:text-white hover:bg-white/5"
156
+ }`}
157
+ >
158
+ <CheckCircle2 className="h-4 w-4" />
159
+ {closedCount} Closed
160
+ </button>
161
+ </div>
162
+
163
+ <div className="flex items-center gap-3 text-sm text-gray-500">
164
+ <button className="flex items-center gap-1 hover:text-white transition-colors">
165
+ <User className="h-3.5 w-3.5" />
166
+ Author
167
+ </button>
168
+ <button className="flex items-center gap-1 hover:text-white transition-colors">
169
+ <Tag className="h-3.5 w-3.5" />
170
+ Label
171
+ </button>
172
+ <button className="flex items-center gap-1 hover:text-white transition-colors">
173
+ <ArrowUpDown className="h-3.5 w-3.5" />
174
+ Sort
175
+ </button>
176
+ </div>
177
+ </div>
178
+
179
+ {/* Issues List */}
180
+ <div className="divide-y divide-white/5">
181
+ <AnimatePresence mode="popLayout">
182
+ {filteredIssues.length > 0 ? (
183
+ filteredIssues.map((issue, index) => (
184
+ <motion.a
185
+ key={issue.id}
186
+ href={`/${repoOwner}/${repoName}/issues/${issue.number}`}
187
+ initial={{ opacity: 0, x: -20 }}
188
+ animate={{ opacity: 1, x: 0 }}
189
+ exit={{ opacity: 0, x: 20 }}
190
+ transition={{ delay: index * 0.03 }}
191
+ className="flex items-start gap-3 p-4 hover:bg-white/[0.02] transition-colors group"
192
+ >
193
+ {/* Status Icon */}
194
+ <div className="mt-0.5">
195
+ {issue.state === "open" ? (
196
+ <div className="relative">
197
+ <AlertCircle className="h-5 w-5 text-green-500" />
198
+ <div className="absolute inset-0 bg-green-500 blur-md opacity-0 group-hover:opacity-30 transition-opacity" />
199
+ </div>
200
+ ) : (
201
+ <div className="relative">
202
+ <CheckCircle2 className="h-5 w-5 text-purple-500" />
203
+ <div className="absolute inset-0 bg-purple-500 blur-md opacity-0 group-hover:opacity-30 transition-opacity" />
204
+ </div>
205
+ )}
206
+ </div>
207
+
208
+ {/* Content */}
209
+ <div className="flex-1 min-w-0">
210
+ <div className="flex items-center gap-2 mb-1">
211
+ <span className="font-semibold text-white group-hover:text-cyan-400 transition-colors truncate">
212
+ {issue.title}
213
+ </span>
214
+ {/* Labels */}
215
+ {issue.labels?.map(label => (
216
+ <span
217
+ key={label.name}
218
+ className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"
219
+ style={{
220
+ backgroundColor: `${label.color}20`,
221
+ color: label.color,
222
+ border: `1px solid ${label.color}40`
223
+ }}
224
+ >
225
+ {label.name}
226
+ </span>
227
+ ))}
228
+ </div>
229
+ <div className="flex items-center gap-2 text-xs text-gray-500">
230
+ <span>#{issue.number}</span>
231
+ <span>•</span>
232
+ <span>opened {timeAgo(issue.createdAt)}</span>
233
+ <span>•</span>
234
+ <span className="flex items-center gap-1">
235
+ {issue.author.avatarUrl ? (
236
+ <img
237
+ src={issue.author.avatarUrl}
238
+ alt=""
239
+ className="h-4 w-4 rounded-full"
240
+ />
241
+ ) : (
242
+ <div className="h-4 w-4 rounded-full bg-gradient-to-br from-cyan-400 to-blue-500 flex items-center justify-center text-[8px] text-white font-bold">
243
+ {issue.author.username[0].toUpperCase()}
244
+ </div>
245
+ )}
246
+ <span className="hover:text-cyan-400 transition-colors">
247
+ {issue.author.username}
248
+ </span>
249
+ </span>
250
+ </div>
251
+ </div>
252
+
253
+ {/* Comment Count */}
254
+ {issue.commentCount > 0 && (
255
+ <div className="flex items-center gap-1 text-xs text-gray-500 group-hover:text-gray-400 transition-colors">
256
+ <MessageSquare className="h-4 w-4" />
257
+ {issue.commentCount}
258
+ </div>
259
+ )}
260
+ </motion.a>
261
+ ))
262
+ ) : (
263
+ <motion.div
264
+ initial={{ opacity: 0 }}
265
+ animate={{ opacity: 1 }}
266
+ className="p-12 text-center"
267
+ >
268
+ <div className="relative inline-block mb-4">
269
+ <Sparkles className="h-12 w-12 text-cyan-400" />
270
+ <div className="absolute inset-0 bg-cyan-500 blur-xl opacity-30" />
271
+ </div>
272
+ <h3 className="text-lg font-semibold text-white mb-2">
273
+ {searchQuery ? "No matching issues" : "Welcome to issues!"}
274
+ </h3>
275
+ <p className="text-gray-500 mb-6 max-w-md mx-auto">
276
+ {searchQuery
277
+ ? "Try adjusting your search or filter to find what you're looking for."
278
+ : "Issues are used to track todos, bugs, feature requests, and more."}
279
+ </p>
280
+ {!searchQuery && (
281
+ <motion.a
282
+ href={`/${repoOwner}/${repoName}/issues/new`}
283
+ whileHover={{ scale: 1.05 }}
284
+ whileTap={{ scale: 0.95 }}
285
+ className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-cyan-500 to-blue-600 text-sm font-medium text-white shadow-lg shadow-cyan-500/25"
286
+ >
287
+ <Plus className="h-4 w-4" />
288
+ Create the first issue
289
+ </motion.a>
290
+ )}
291
+ </motion.div>
292
+ )}
293
+ </AnimatePresence>
294
+ </div>
295
+ </div>
296
+ </motion.div>
297
+ </div>
298
+ );
299
+}
0
300
▼src/components/layout/Header.astro
+6-6
39
39
<a href="/actions" class="text-muted-foreground transition-colors hover:text-foreground">
40
40
Actions
41
41
</a>
42
+ <a href="/admin" class="text-muted-foreground transition-colors hover:text-purple-400 font-semibold flex items-center gap-1">
43
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
44
+ Mission Control
45
+ </a>
42
46
</nav>
43
47
44
48
<!-- Spacer -->
75
79
<a href="/new" class="flex items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
76
80
New Repository
77
81
</a>
78
- <a href="/new/organization" class="flex items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
79
- New Organization
80
- </a>
82
+
81
83
<a href="/new/import" class="flex items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
82
84
Import Repository
83
85
</a>
120
122
<a href="/repositories" class="flex items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
121
123
Your Repositories
122
124
</a>
123
- <a href="/organizations" class="flex items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
124
- Your Organizations
125
- </a>
125
+
126
126
<a href="/stars" class="flex items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
127
127
Your Stars
128
128
</a>
129
129
▼src/components/milestones/MilestonesList.tsx
+276-0
1
+"use client";
2
+import { motion, AnimatePresence } from "framer-motion";
3
+import {
4
+ Target,
5
+ Calendar,
6
+ CheckCircle2,
7
+ Circle,
8
+ Plus,
9
+ Clock,
10
+ AlertCircle,
11
+ Milestone,
12
+ TrendingUp,
13
+ MoreHorizontal,
14
+ Edit3,
15
+ Trash2,
16
+ Archive
17
+} from "lucide-react";
18
+import { useState } from "react";
19
+
20
+interface MilestoneItem {
21
+ id: string;
22
+ title: string;
23
+ description?: string;
24
+ dueDate?: string;
25
+ state: "open" | "closed";
26
+ openIssues: number;
27
+ closedIssues: number;
28
+ progress: number;
29
+}
30
+
31
+interface Props {
32
+ milestones: MilestoneItem[];
33
+ repoOwner: string;
34
+ repoName: string;
35
+}
36
+
37
+function formatDate(date: string): string {
38
+ return new Date(date).toLocaleDateString('en-US', {
39
+ month: 'short',
40
+ day: 'numeric',
41
+ year: 'numeric'
42
+ });
43
+}
44
+
45
+function getDaysRemaining(dueDate: string): { text: string; color: string } {
46
+ const now = new Date();
47
+ const due = new Date(dueDate);
48
+ const diffDays = Math.ceil((due.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
49
+
50
+ if (diffDays < 0) return { text: `${Math.abs(diffDays)} days overdue`, color: "text-red-400" };
51
+ if (diffDays === 0) return { text: "Due today", color: "text-orange-400" };
52
+ if (diffDays <= 7) return { text: `${diffDays} days left`, color: "text-yellow-400" };
53
+ return { text: `${diffDays} days left`, color: "text-green-400" };
54
+}
55
+
56
+export default function MilestonesList({ milestones, repoOwner, repoName }: Props) {
57
+ const [filter, setFilter] = useState<"open" | "closed">("open");
58
+ const [showMenu, setShowMenu] = useState<string | null>(null);
59
+
60
+ const filteredMilestones = milestones.filter(m => m.state === filter);
61
+ const openCount = milestones.filter(m => m.state === "open").length;
62
+ const closedCount = milestones.filter(m => m.state === "closed").length;
63
+
64
+ return (
65
+ <div className="space-y-6">
66
+ {/* Header */}
67
+ <motion.div
68
+ initial={{ opacity: 0, y: -20 }}
69
+ animate={{ opacity: 1, y: 0 }}
70
+ className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"
71
+ >
72
+ <div>
73
+ <h1 className="text-2xl font-bold text-white flex items-center gap-3">
74
+ <div className="p-2 rounded-lg bg-gradient-to-br from-purple-500/20 to-pink-500/20 border border-purple-500/30">
75
+ <Target className="h-6 w-6 text-purple-400" />
76
+ </div>
77
+ Milestones
78
+ </h1>
79
+ <p className="text-gray-500 text-sm mt-1">
80
+ Track progress and manage project goals
81
+ </p>
82
+ </div>
83
+
84
+ <motion.a
85
+ href={`/${repoOwner}/${repoName}/milestones/new`}
86
+ whileHover={{ scale: 1.02 }}
87
+ whileTap={{ scale: 0.98 }}
88
+ className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-white shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all"
89
+ >
90
+ <Plus className="h-4 w-4" />
91
+ New Milestone
92
+ </motion.a>
93
+ </motion.div>
94
+
95
+ {/* Filter Tabs */}
96
+ <motion.div
97
+ initial={{ opacity: 0 }}
98
+ animate={{ opacity: 1 }}
99
+ transition={{ delay: 0.1 }}
100
+ className="flex items-center gap-2"
101
+ >
102
+ <button
103
+ onClick={() => setFilter("open")}
104
+ className={`inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all ${filter === "open"
105
+ ? "bg-green-500/10 text-green-400 border border-green-500/30"
106
+ : "text-gray-400 hover:text-white hover:bg-white/5"
107
+ }`}
108
+ >
109
+ <AlertCircle className="h-4 w-4" />
110
+ {openCount} Open
111
+ </button>
112
+ <button
113
+ onClick={() => setFilter("closed")}
114
+ className={`inline-flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-all ${filter === "closed"
115
+ ? "bg-purple-500/10 text-purple-400 border border-purple-500/30"
116
+ : "text-gray-400 hover:text-white hover:bg-white/5"
117
+ }`}
118
+ >
119
+ <CheckCircle2 className="h-4 w-4" />
120
+ {closedCount} Closed
121
+ </button>
122
+ </motion.div>
123
+
124
+ {/* Milestones List */}
125
+ <div className="space-y-4">
126
+ <AnimatePresence mode="popLayout">
127
+ {filteredMilestones.length > 0 ? (
128
+ filteredMilestones.map((milestone, index) => (
129
+ <motion.div
130
+ key={milestone.id}
131
+ initial={{ opacity: 0, y: 20 }}
132
+ animate={{ opacity: 1, y: 0 }}
133
+ exit={{ opacity: 0, y: -20 }}
134
+ transition={{ delay: index * 0.05 }}
135
+ className="relative group"
136
+ >
137
+ {/* Gradient border */}
138
+ <div className="absolute -inset-[1px] bg-gradient-to-r from-purple-500/20 via-transparent to-pink-500/20 rounded-xl opacity-50 group-hover:opacity-100 transition-opacity" />
139
+
140
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-5">
141
+ <div className="flex items-start justify-between gap-4">
142
+ <div className="flex-1">
143
+ {/* Title */}
144
+ <a
145
+ href={`/${repoOwner}/${repoName}/milestones/${milestone.id}`}
146
+ className="text-lg font-semibold text-white hover:text-purple-400 transition-colors"
147
+ >
148
+ {milestone.title}
149
+ </a>
150
+
151
+ {/* Description */}
152
+ {milestone.description && (
153
+ <p className="text-sm text-gray-400 mt-1 line-clamp-2">
154
+ {milestone.description}
155
+ </p>
156
+ )}
157
+
158
+ {/* Meta Info */}
159
+ <div className="flex items-center gap-4 mt-3 text-xs text-gray-500">
160
+ {milestone.dueDate && (
161
+ <span className="flex items-center gap-1">
162
+ <Calendar className="h-3.5 w-3.5" />
163
+ Due {formatDate(milestone.dueDate)}
164
+ </span>
165
+ )}
166
+ {milestone.dueDate && (
167
+ <span className={`flex items-center gap-1 ${getDaysRemaining(milestone.dueDate).color}`}>
168
+ <Clock className="h-3.5 w-3.5" />
169
+ {getDaysRemaining(milestone.dueDate).text}
170
+ </span>
171
+ )}
172
+ </div>
173
+ </div>
174
+
175
+ {/* Actions Menu */}
176
+ <div className="relative">
177
+ <button
178
+ onClick={() => setShowMenu(showMenu === milestone.id ? null : milestone.id)}
179
+ className="p-2 rounded-lg hover:bg-white/10 transition-colors"
180
+ >
181
+ <MoreHorizontal className="h-4 w-4 text-gray-500" />
182
+ </button>
183
+
184
+ {showMenu === milestone.id && (
185
+ <motion.div
186
+ initial={{ opacity: 0, scale: 0.95 }}
187
+ animate={{ opacity: 1, scale: 1 }}
188
+ className="absolute right-0 top-full mt-1 w-40 rounded-lg border border-white/10 bg-[#161b22] shadow-xl z-10"
189
+ >
190
+ <div className="p-1">
191
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-white/5 rounded-md">
192
+ <Edit3 className="h-4 w-4" />
193
+ Edit
194
+ </button>
195
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-300 hover:bg-white/5 rounded-md">
196
+ <Archive className="h-4 w-4" />
197
+ Close
198
+ </button>
199
+ <button className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-md">
200
+ <Trash2 className="h-4 w-4" />
201
+ Delete
202
+ </button>
203
+ </div>
204
+ </motion.div>
205
+ )}
206
+ </div>
207
+ </div>
208
+
209
+ {/* Progress Section */}
210
+ <div className="mt-4">
211
+ <div className="flex items-center justify-between text-xs text-gray-500 mb-2">
212
+ <span className="flex items-center gap-3">
213
+ <span className="flex items-center gap-1 text-green-400">
214
+ <CheckCircle2 className="h-3.5 w-3.5" />
215
+ {milestone.closedIssues} closed
216
+ </span>
217
+ <span className="flex items-center gap-1">
218
+ <Circle className="h-3.5 w-3.5" />
219
+ {milestone.openIssues} open
220
+ </span>
221
+ </span>
222
+ <span className="flex items-center gap-1 text-purple-400">
223
+ <TrendingUp className="h-3.5 w-3.5" />
224
+ {milestone.progress}% complete
225
+ </span>
226
+ </div>
227
+
228
+ {/* Progress Bar */}
229
+ <div className="h-2 bg-white/5 rounded-full overflow-hidden">
230
+ <motion.div
231
+ initial={{ width: 0 }}
232
+ animate={{ width: `${milestone.progress}%` }}
233
+ transition={{ duration: 0.8, ease: "easeOut" }}
234
+ className="h-full rounded-full bg-gradient-to-r from-purple-500 to-pink-500"
235
+ />
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </motion.div>
240
+ ))
241
+ ) : (
242
+ <motion.div
243
+ initial={{ opacity: 0 }}
244
+ animate={{ opacity: 1 }}
245
+ className="text-center py-16"
246
+ >
247
+ <div className="relative inline-block mb-4">
248
+ <Milestone className="h-16 w-16 text-purple-400" />
249
+ <div className="absolute inset-0 bg-purple-500 blur-xl opacity-30" />
250
+ </div>
251
+ <h3 className="text-xl font-semibold text-white mb-2">
252
+ {filter === "closed" ? "No closed milestones" : "No milestones yet"}
253
+ </h3>
254
+ <p className="text-gray-500 mb-6 max-w-md mx-auto">
255
+ {filter === "closed"
256
+ ? "Closed milestones will appear here."
257
+ : "Milestones help you track progress on groups of issues and pull requests."}
258
+ </p>
259
+ {filter === "open" && (
260
+ <motion.a
261
+ href={`/${repoOwner}/${repoName}/milestones/new`}
262
+ whileHover={{ scale: 1.05 }}
263
+ whileTap={{ scale: 0.95 }}
264
+ className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-white shadow-lg shadow-purple-500/25"
265
+ >
266
+ <Plus className="h-4 w-4" />
267
+ Create a milestone
268
+ </motion.a>
269
+ )}
270
+ </motion.div>
271
+ )}
272
+ </AnimatePresence>
273
+ </div>
274
+ </div>
275
+ );
276
+}
0
277
▼src/components/milestones/NewMilestoneForm.tsx
+169-0
1
+"use client";
2
+import { motion } from "framer-motion";
3
+import {
4
+ Target,
5
+ Calendar,
6
+ FileText,
7
+ ArrowLeft,
8
+ Save,
9
+ X
10
+} from "lucide-react";
11
+import { useState } from "react";
12
+
13
+interface Props {
14
+ repoOwner: string;
15
+ repoName: string;
16
+}
17
+
18
+export default function NewMilestoneForm({ repoOwner, repoName }: Props) {
19
+ const [title, setTitle] = useState("");
20
+ const [description, setDescription] = useState("");
21
+ const [dueDate, setDueDate] = useState("");
22
+ const [isSubmitting, setIsSubmitting] = useState(false);
23
+
24
+ const handleSubmit = async (e: React.FormEvent) => {
25
+ e.preventDefault();
26
+ if (!title.trim()) return;
27
+
28
+ setIsSubmitting(true);
29
+
30
+ try {
31
+ const res = await fetch(`/api/repos/${repoOwner}/${repoName}/milestones`, {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify({
35
+ title: title.trim(),
36
+ description: description.trim() || null,
37
+ dueOn: dueDate || null,
38
+ }),
39
+ });
40
+
41
+ if (res.ok) {
42
+ window.location.href = `/${repoOwner}/${repoName}/milestones`;
43
+ } else {
44
+ alert("Failed to create milestone");
45
+ }
46
+ } catch (error) {
47
+ console.error("Error creating milestone:", error);
48
+ alert("Failed to create milestone");
49
+ } finally {
50
+ setIsSubmitting(false);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <div className="max-w-3xl mx-auto">
56
+ {/* Header */}
57
+ <motion.div
58
+ initial={{ opacity: 0, y: -20 }}
59
+ animate={{ opacity: 1, y: 0 }}
60
+ className="mb-8"
61
+ >
62
+ <a
63
+ href={`/${repoOwner}/${repoName}/milestones`}
64
+ className="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-white transition-colors mb-4"
65
+ >
66
+ <ArrowLeft className="h-4 w-4" />
67
+ Back to milestones
68
+ </a>
69
+
70
+ <h1 className="text-2xl font-bold text-white flex items-center gap-3">
71
+ <div className="p-2 rounded-lg bg-gradient-to-br from-purple-500/20 to-pink-500/20 border border-purple-500/30">
72
+ <Target className="h-6 w-6 text-purple-400" />
73
+ </div>
74
+ New Milestone
75
+ </h1>
76
+ <p className="text-gray-500 text-sm mt-2">
77
+ Create a milestone to track progress on a collection of issues and pull requests.
78
+ </p>
79
+ </motion.div>
80
+
81
+ {/* Form */}
82
+ <motion.form
83
+ initial={{ opacity: 0, y: 20 }}
84
+ animate={{ opacity: 1, y: 0 }}
85
+ transition={{ delay: 0.1 }}
86
+ onSubmit={handleSubmit}
87
+ className="space-y-6"
88
+ >
89
+ {/* Title */}
90
+ <div className="relative">
91
+ <div className="absolute -inset-[1px] bg-gradient-to-r from-purple-500/20 via-transparent to-pink-500/20 rounded-xl opacity-50" />
92
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6">
93
+ <label className="block text-sm font-medium text-white mb-2 flex items-center gap-2">
94
+ <FileText className="h-4 w-4 text-purple-400" />
95
+ Title *
96
+ </label>
97
+ <input
98
+ type="text"
99
+ value={title}
100
+ onChange={(e) => setTitle(e.target.value)}
101
+ placeholder="Milestone title"
102
+ required
103
+ className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all"
104
+ />
105
+ </div>
106
+ </div>
107
+
108
+ {/* Due Date */}
109
+ <div className="relative">
110
+ <div className="absolute -inset-[1px] bg-gradient-to-r from-cyan-500/20 via-transparent to-blue-500/20 rounded-xl opacity-50" />
111
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6">
112
+ <label className="block text-sm font-medium text-white mb-2 flex items-center gap-2">
113
+ <Calendar className="h-4 w-4 text-cyan-400" />
114
+ Due date (optional)
115
+ </label>
116
+ <input
117
+ type="date"
118
+ value={dueDate}
119
+ onChange={(e) => setDueDate(e.target.value)}
120
+ className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 transition-all [color-scheme:dark]"
121
+ />
122
+ <p className="text-xs text-gray-500 mt-2">
123
+ Set a target date for this milestone to track deadlines.
124
+ </p>
125
+ </div>
126
+ </div>
127
+
128
+ {/* Description */}
129
+ <div className="relative">
130
+ <div className="absolute -inset-[1px] bg-gradient-to-r from-green-500/20 via-transparent to-emerald-500/20 rounded-xl opacity-50" />
131
+ <div className="relative rounded-xl border border-white/10 bg-[#0d1117]/80 backdrop-blur-sm p-6">
132
+ <label className="block text-sm font-medium text-white mb-2 flex items-center gap-2">
133
+ <FileText className="h-4 w-4 text-green-400" />
134
+ Description (optional)
135
+ </label>
136
+ <textarea
137
+ value={description}
138
+ onChange={(e) => setDescription(e.target.value)}
139
+ placeholder="Describe the goals of this milestone..."
140
+ rows={4}
141
+ className="w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder:text-gray-600 focus:outline-none focus:border-green-500/50 focus:ring-2 focus:ring-green-500/20 transition-all resize-none"
142
+ />
143
+ </div>
144
+ </div>
145
+
146
+ {/* Action Buttons */}
147
+ <div className="flex items-center justify-end gap-3 pt-4">
148
+ <a
149
+ href={`/${repoOwner}/${repoName}/milestones`}
150
+ className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg border border-white/10 bg-white/5 text-sm font-medium text-gray-300 hover:bg-white/10 transition-all"
151
+ >
152
+ <X className="h-4 w-4" />
153
+ Cancel
154
+ </a>
155
+ <motion.button
156
+ type="submit"
157
+ disabled={!title.trim() || isSubmitting}
158
+ whileHover={{ scale: 1.02 }}
159
+ whileTap={{ scale: 0.98 }}
160
+ className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 text-sm font-medium text-white shadow-lg shadow-purple-500/25 hover:shadow-purple-500/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
161
+ >
162
+ <Save className="h-4 w-4" />
163
+ {isSubmitting ? "Creating..." : "Create Milestone"}
164
+ </motion.button>
165
+ </div>
166
+ </motion.form>
167
+ </div>
168
+ );
169
+}
0
170
▼src/components/pr/AIReviewPanel.tsx
+348-0
1
+/**
2
+ * AI Review Panel Component
3
+ * Display AI review suggestions with apply/dismiss actions
4
+ */
5
+
6
+import React, { useState } from "react";
7
+import {
8
+ Bot,
9
+ AlertTriangle,
10
+ AlertCircle,
11
+ Info,
12
+ XCircle,
13
+ Check,
14
+ X,
15
+ ChevronDown,
16
+ ChevronRight,
17
+ Sparkles,
18
+ Loader2,
19
+ RefreshCw,
20
+} from "lucide-react";
21
+
22
+interface Suggestion {
23
+ id: string;
24
+ path: string;
25
+ line?: number;
26
+ endLine?: number;
27
+ severity: "info" | "warning" | "error" | "critical";
28
+ type: string;
29
+ title: string;
30
+ message: string;
31
+ suggestedFix?: string;
32
+ explanation?: string;
33
+ isApplied?: boolean;
34
+ isDismissed?: boolean;
35
+}
36
+
37
+interface AIReviewPanelProps {
38
+ reviewId: string;
39
+ status: "pending" | "running" | "completed" | "failed";
40
+ model: string;
41
+ summary?: string;
42
+ suggestions: Suggestion[];
43
+ onApply?: (suggestionId: string) => Promise<void>;
44
+ onDismiss?: (suggestionId: string, reason?: string) => Promise<void>;
45
+ onRefresh?: () => Promise<void>;
46
+}
47
+
48
+const severityConfig = {
49
+ info: {
50
+ icon: Info,
51
+ color: "text-blue-500",
52
+ bg: "bg-blue-500/10",
53
+ border: "border-blue-500/20",
54
+ },
55
+ warning: {
56
+ icon: AlertTriangle,
57
+ color: "text-yellow-500",
58
+ bg: "bg-yellow-500/10",
59
+ border: "border-yellow-500/20",
60
+ },
61
+ error: {
62
+ icon: AlertCircle,
63
+ color: "text-orange-500",
64
+ bg: "bg-orange-500/10",
65
+ border: "border-orange-500/20",
66
+ },
67
+ critical: {
68
+ icon: XCircle,
69
+ color: "text-red-500",
70
+ bg: "bg-red-500/10",
71
+ border: "border-red-500/20",
72
+ },
73
+};
74
+
75
+export function AIReviewPanel({
76
+ reviewId,
77
+ status,
78
+ model,
79
+ summary,
80
+ suggestions,
81
+ onApply,
82
+ onDismiss,
83
+ onRefresh,
84
+}: AIReviewPanelProps) {
85
+ const [expandedSuggestions, setExpandedSuggestions] = useState<Set<string>>(
86
+ new Set()
87
+ );
88
+ const [applyingId, setApplyingId] = useState<string | null>(null);
89
+ const [dismissingId, setDismissingId] = useState<string | null>(null);
90
+
91
+ const toggleExpanded = (id: string) => {
92
+ const newExpanded = new Set(expandedSuggestions);
93
+ if (newExpanded.has(id)) {
94
+ newExpanded.delete(id);
95
+ } else {
96
+ newExpanded.add(id);
97
+ }
98
+ setExpandedSuggestions(newExpanded);
99
+ };
100
+
101
+ const handleApply = async (suggestionId: string) => {
102
+ if (!onApply) return;
103
+ setApplyingId(suggestionId);
104
+ try {
105
+ await onApply(suggestionId);
106
+ } finally {
107
+ setApplyingId(null);
108
+ }
109
+ };
110
+
111
+ const handleDismiss = async (suggestionId: string) => {
112
+ if (!onDismiss) return;
113
+ setDismissingId(suggestionId);
114
+ try {
115
+ await onDismiss(suggestionId);
116
+ } finally {
117
+ setDismissingId(null);
118
+ }
119
+ };
120
+
121
+ const activeSuggestions = suggestions.filter(
122
+ (s) => !s.isApplied && !s.isDismissed
123
+ );
124
+ const resolvedSuggestions = suggestions.filter(
125
+ (s) => s.isApplied || s.isDismissed
126
+ );
127
+
128
+ const severityCounts = activeSuggestions.reduce(
129
+ (acc, s) => {
130
+ acc[s.severity] = (acc[s.severity] || 0) + 1;
131
+ return acc;
132
+ },
133
+ {} as Record<string, number>
134
+ );
135
+
136
+ return (
137
+ <div className="rounded-lg border bg-card text-card-foreground shadow-sm">
138
+ {/* Header */}
139
+ <div className="flex items-center justify-between border-b p-4">
140
+ <div className="flex items-center gap-3">
141
+ <div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10">
142
+ <Bot className="h-5 w-5 text-primary" />
143
+ </div>
144
+ <div>
145
+ <h3 className="font-semibold flex items-center gap-2">
146
+ AI Code Review
147
+ <Sparkles className="h-4 w-4 text-yellow-500" />
148
+ </h3>
149
+ <p className="text-sm text-muted-foreground">
150
+ Powered by {model}
151
+ </p>
152
+ </div>
153
+ </div>
154
+
155
+ <div className="flex items-center gap-2">
156
+ {status === "running" && (
157
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
158
+ <Loader2 className="h-4 w-4 animate-spin" />
159
+ Analyzing...
160
+ </div>
161
+ )}
162
+ {status === "completed" && onRefresh && (
163
+ <button
164
+ onClick={onRefresh}
165
+ className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
166
+ >
167
+ <RefreshCw className="h-4 w-4" />
168
+ Re-run
169
+ </button>
170
+ )}
171
+ </div>
172
+ </div>
173
+
174
+ {/* Status */}
175
+ {status === "pending" && (
176
+ <div className="p-6 text-center text-muted-foreground">
177
+ <Loader2 className="h-8 w-8 animate-spin mx-auto mb-2" />
178
+ <p>Preparing AI review...</p>
179
+ </div>
180
+ )}
181
+
182
+ {status === "failed" && (
183
+ <div className="p-6 text-center">
184
+ <XCircle className="h-8 w-8 text-red-500 mx-auto mb-2" />
185
+ <p className="text-red-500 font-medium">Review failed</p>
186
+ <p className="text-sm text-muted-foreground mt-1">
187
+ Please try again or check your configuration
188
+ </p>
189
+ </div>
190
+ )}
191
+
192
+ {status === "completed" && (
193
+ <>
194
+ {/* Summary */}
195
+ {summary && (
196
+ <div className="p-4 border-b bg-muted/30">
197
+ <p className="text-sm">{summary}</p>
198
+ </div>
199
+ )}
200
+
201
+ {/* Stats */}
202
+ <div className="flex items-center gap-4 p-4 border-b">
203
+ <div className="text-sm text-muted-foreground">
204
+ {activeSuggestions.length} active suggestions
205
+ </div>
206
+ <div className="flex items-center gap-2">
207
+ {severityCounts.critical && (
208
+ <span className="flex items-center gap-1 text-xs text-red-500">
209
+ <XCircle className="h-3 w-3" />
210
+ {severityCounts.critical} critical
211
+ </span>
212
+ )}
213
+ {severityCounts.error && (
214
+ <span className="flex items-center gap-1 text-xs text-orange-500">
215
+ <AlertCircle className="h-3 w-3" />
216
+ {severityCounts.error} errors
217
+ </span>
218
+ )}
219
+ {severityCounts.warning && (
220
+ <span className="flex items-center gap-1 text-xs text-yellow-500">
221
+ <AlertTriangle className="h-3 w-3" />
222
+ {severityCounts.warning} warnings
223
+ </span>
224
+ )}
225
+ </div>
226
+ </div>
227
+
228
+ {/* Suggestions List */}
229
+ <div className="divide-y">
230
+ {activeSuggestions.length === 0 ? (
231
+ <div className="p-6 text-center text-muted-foreground">
232
+ <Check className="h-8 w-8 text-green-500 mx-auto mb-2" />
233
+ <p>All clear! No issues found.</p>
234
+ </div>
235
+ ) : (
236
+ activeSuggestions.map((suggestion) => {
237
+ const config = severityConfig[suggestion.severity];
238
+ const Icon = config.icon;
239
+ const isExpanded = expandedSuggestions.has(suggestion.id);
240
+
241
+ return (
242
+ <div
243
+ key={suggestion.id}
244
+ className={`${config.bg} ${config.border} border-l-4`}
245
+ >
246
+ {/* Suggestion Header */}
247
+ <button
248
+ className="w-full flex items-start gap-3 p-4 text-left hover:bg-muted/50 transition-colors"
249
+ onClick={() => toggleExpanded(suggestion.id)}
250
+ >
251
+ <Icon className={`h-5 w-5 mt-0.5 ${config.color}`} />
252
+ <div className="flex-1 min-w-0">
253
+ <div className="flex items-center gap-2">
254
+ <span className="font-medium">{suggestion.title}</span>
255
+ <span className="text-xs bg-muted px-1.5 py-0.5 rounded">
256
+ {suggestion.type}
257
+ </span>
258
+ </div>
259
+ <div className="text-sm text-muted-foreground mt-1">
260
+ <code className="bg-muted px-1 rounded">
261
+ {suggestion.path}
262
+ {suggestion.line && `:${suggestion.line}`}
263
+ </code>
264
+ </div>
265
+ </div>
266
+ {isExpanded ? (
267
+ <ChevronDown className="h-5 w-5 text-muted-foreground" />
268
+ ) : (
269
+ <ChevronRight className="h-5 w-5 text-muted-foreground" />
270
+ )}
271
+ </button>
272
+
273
+ {/* Expanded Content */}
274
+ {isExpanded && (
275
+ <div className="px-4 pb-4 pl-12">
276
+ <p className="text-sm mb-3">{suggestion.message}</p>
277
+
278
+ {suggestion.explanation && (
279
+ <div className="text-sm text-muted-foreground mb-3 p-2 bg-muted/50 rounded">
280
+ <strong>Why:</strong> {suggestion.explanation}
281
+ </div>
282
+ )}
283
+
284
+ {suggestion.suggestedFix && (
285
+ <div className="mb-3">
286
+ <p className="text-xs text-muted-foreground mb-1">
287
+ Suggested fix:
288
+ </p>
289
+ <pre className="text-sm bg-muted p-2 rounded overflow-x-auto">
290
+ <code>{suggestion.suggestedFix}</code>
291
+ </pre>
292
+ </div>
293
+ )}
294
+
295
+ {/* Actions */}
296
+ <div className="flex items-center gap-2">
297
+ {suggestion.suggestedFix && onApply && (
298
+ <button
299
+ onClick={() => handleApply(suggestion.id)}
300
+ disabled={applyingId === suggestion.id}
301
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 transition-colors"
302
+ >
303
+ {applyingId === suggestion.id ? (
304
+ <Loader2 className="h-4 w-4 animate-spin" />
305
+ ) : (
306
+ <Check className="h-4 w-4" />
307
+ )}
308
+ Apply Fix
309
+ </button>
310
+ )}
311
+ {onDismiss && (
312
+ <button
313
+ onClick={() => handleDismiss(suggestion.id)}
314
+ disabled={dismissingId === suggestion.id}
315
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm border rounded-md hover:bg-muted transition-colors"
316
+ >
317
+ {dismissingId === suggestion.id ? (
318
+ <Loader2 className="h-4 w-4 animate-spin" />
319
+ ) : (
320
+ <X className="h-4 w-4" />
321
+ )}
322
+ Dismiss
323
+ </button>
324
+ )}
325
+ </div>
326
+ </div>
327
+ )}
328
+ </div>
329
+ );
330
+ })
331
+ )}
332
+ </div>
333
+
334
+ {/* Resolved suggestions */}
335
+ {resolvedSuggestions.length > 0 && (
336
+ <div className="border-t p-4">
337
+ <p className="text-sm text-muted-foreground">
338
+ {resolvedSuggestions.length} resolved suggestions
339
+ </p>
340
+ </div>
341
+ )}
342
+ </>
343
+ )}
344
+ </div>
345
+ );
346
+}
347
+
348
+export default AIReviewPanel;
0
349
▼src/components/pr/StackVisualization.tsx
+230-0
1
+/**
2
+ * Stack Visualization Component
3
+ * Visual tree/timeline showing stacked PRs with status and navigation
4
+ */
5
+
6
+import React from "react";
7
+import {
8
+ GitPullRequest,
9
+ GitMerge,
10
+ CircleDot,
11
+ Circle,
12
+ FileEdit,
13
+ ChevronRight,
14
+ Layers,
15
+ ArrowUp
16
+} from "lucide-react";
17
+
18
+interface StackEntry {
19
+ order: number;
20
+ pr: {
21
+ id: string;
22
+ number: number;
23
+ title: string;
24
+ state: string;
25
+ isDraft: boolean;
26
+ headBranch: string;
27
+ isMerged: boolean;
28
+ };
29
+ parentPrId: string | null;
30
+}
31
+
32
+interface StackVisualizationProps {
33
+ stackId: string;
34
+ stackName?: string;
35
+ baseBranch: string;
36
+ entries: StackEntry[];
37
+ currentPrId?: string;
38
+ owner: string;
39
+ repo: string;
40
+ onReorder?: (newOrder: string[]) => void;
41
+ compact?: boolean;
42
+}
43
+
44
+export function StackVisualization({
45
+ stackId,
46
+ stackName,
47
+ baseBranch,
48
+ entries,
49
+ currentPrId,
50
+ owner,
51
+ repo,
52
+ onReorder,
53
+ compact = false,
54
+}: StackVisualizationProps) {
55
+ const getStatusIcon = (entry: StackEntry) => {
56
+ if (entry.pr.isMerged) {
57
+ return <GitMerge className="h-4 w-4 text-purple-500" />;
58
+ }
59
+ if (entry.pr.isDraft) {
60
+ return <FileEdit className="h-4 w-4 text-gray-400" />;
61
+ }
62
+ if (entry.pr.state === "open") {
63
+ return <GitPullRequest className="h-4 w-4 text-green-500" />;
64
+ }
65
+ return <Circle className="h-4 w-4 text-red-500" />;
66
+ };
67
+
68
+ const getStatusColor = (entry: StackEntry) => {
69
+ if (entry.pr.isMerged) return "border-purple-500 bg-purple-500/10";
70
+ if (entry.pr.isDraft) return "border-gray-400 bg-gray-400/10";
71
+ if (entry.pr.state === "open") return "border-green-500 bg-green-500/10";
72
+ return "border-red-500 bg-red-500/10";
73
+ };
74
+
75
+ const sortedEntries = [...entries].sort((a, b) => a.order - b.order);
76
+
77
+ if (compact) {
78
+ return (
79
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
80
+ <Layers className="h-4 w-4" />
81
+ <span>Stack of {entries.length}</span>
82
+ <div className="flex items-center gap-1">
83
+ {sortedEntries.map((entry, i) => (
84
+ <React.Fragment key={entry.pr.id}>
85
+ <a
86
+ href={`/${owner}/${repo}/pulls/${entry.pr.number}`}
87
+ className={`px-1.5 py-0.5 rounded text-xs font-medium transition-colors ${entry.pr.id === currentPrId
88
+ ? "bg-primary text-primary-foreground"
89
+ : "bg-muted hover:bg-muted/80"
90
+ }`}
91
+ >
92
+ #{entry.pr.number}
93
+ </a>
94
+ {i < sortedEntries.length - 1 && (
95
+ <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
96
+ )}
97
+ </React.Fragment>
98
+ ))}
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ return (
105
+ <div className="rounded-lg border bg-card text-card-foreground shadow-sm">
106
+ {/* Header */}
107
+ <div className="flex items-center justify-between border-b p-4">
108
+ <div className="flex items-center gap-2">
109
+ <Layers className="h-5 w-5 text-primary" />
110
+ <h3 className="font-semibold">
111
+ {stackName || "Stacked Changes"}
112
+ </h3>
113
+ <span className="text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full">
114
+ {entries.length} PRs
115
+ </span>
116
+ </div>
117
+ <div className="text-xs text-muted-foreground">
118
+ Base: <code className="bg-muted px-1.5 py-0.5 rounded">{baseBranch}</code>
119
+ </div>
120
+ </div>
121
+
122
+ {/* Stack Visualization */}
123
+ <div className="p-4">
124
+ <div className="relative">
125
+ {/* Vertical line */}
126
+ <div className="absolute left-4 top-0 bottom-0 w-0.5 bg-border" />
127
+
128
+ {/* Base branch indicator */}
129
+ <div className="relative flex items-center gap-3 mb-4">
130
+ <div className="relative z-10 flex h-8 w-8 items-center justify-center rounded-full border-2 border-primary bg-background">
131
+ <CircleDot className="h-4 w-4 text-primary" />
132
+ </div>
133
+ <div className="flex-1">
134
+ <div className="text-sm font-medium">{baseBranch}</div>
135
+ <div className="text-xs text-muted-foreground">Base branch</div>
136
+ </div>
137
+ </div>
138
+
139
+ {/* Stack entries */}
140
+ {sortedEntries.map((entry, index) => {
141
+ const isCurrent = entry.pr.id === currentPrId;
142
+ const isLast = index === sortedEntries.length - 1;
143
+
144
+ return (
145
+ <div
146
+ key={entry.pr.id}
147
+ className={`relative flex items-start gap-3 ${isLast ? "" : "mb-3"
148
+ }`}
149
+ >
150
+ {/* Arrow indicator */}
151
+ <div className="absolute -left-0.5 -top-6 flex items-center justify-center w-9">
152
+ <ArrowUp className="h-4 w-4 text-muted-foreground/50" />
153
+ </div>
154
+
155
+ {/* Node */}
156
+ <div
157
+ className={`relative z-10 flex h-8 w-8 items-center justify-center rounded-full border-2 bg-background transition-all ${isCurrent
158
+ ? "ring-2 ring-primary ring-offset-2 ring-offset-background"
159
+ : ""
160
+ } ${getStatusColor(entry)}`}
161
+ >
162
+ {getStatusIcon(entry)}
163
+ </div>
164
+
165
+ {/* PR Info */}
166
+ <a
167
+ href={`/${owner}/${repo}/pulls/${entry.pr.number}`}
168
+ className={`flex-1 rounded-lg border p-3 transition-all hover:shadow-md ${isCurrent
169
+ ? "border-primary bg-primary/5"
170
+ : "hover:border-primary/50"
171
+ }`}
172
+ >
173
+ <div className="flex items-start justify-between gap-2">
174
+ <div className="flex-1 min-w-0">
175
+ <div className="flex items-center gap-2">
176
+ <span className="text-sm font-medium text-muted-foreground">
177
+ #{entry.pr.number}
178
+ </span>
179
+ {entry.pr.isDraft && (
180
+ <span className="text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded">
181
+ Draft
182
+ </span>
183
+ )}
184
+ {entry.pr.isMerged && (
185
+ <span className="text-xs bg-purple-500/10 text-purple-500 px-1.5 py-0.5 rounded">
186
+ Merged
187
+ </span>
188
+ )}
189
+ {isCurrent && (
190
+ <span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded">
191
+ Current
192
+ </span>
193
+ )}
194
+ </div>
195
+ <h4 className="font-medium mt-1 line-clamp-1">
196
+ {entry.pr.title}
197
+ </h4>
198
+ <div className="text-xs text-muted-foreground mt-1">
199
+ <code className="bg-muted px-1 py-0.5 rounded">
200
+ {entry.pr.headBranch}
201
+ </code>
202
+ </div>
203
+ </div>
204
+ <div className="text-xs text-muted-foreground">
205
+ {entry.order} of {entries.length}
206
+ </div>
207
+ </div>
208
+ </a>
209
+ </div>
210
+ );
211
+ })}
212
+ </div>
213
+ </div>
214
+
215
+ {/* Actions */}
216
+ <div className="border-t p-3 flex items-center justify-between">
217
+ <div className="text-xs text-muted-foreground">
218
+ {sortedEntries.filter((e) => e.pr.isMerged).length} of {entries.length} merged
219
+ </div>
220
+ <div className="flex items-center gap-2">
221
+ <button className="text-xs text-primary hover:underline">
222
+ View all changes
223
+ </button>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ );
228
+}
229
+
230
+export default StackVisualization;
0
231