aviol commited on
Commit
d51321a
·
1 Parent(s): c56a66b

Initial commit for uvify UI

Browse files
.dockerignore ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node.js
2
+ node_modules/
3
+ npm-debug.log*
4
+ .npm
5
+
6
+ # Python
7
+ .venv/
8
+ __pycache__/
9
+ *.pyc
10
+ *.pyo
11
+ *.pyd
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+
29
+ # IDEs
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+ *~
35
+
36
+ # OS
37
+ .DS_Store
38
+ Thumbs.db
39
+
40
+ # Git
41
+ .git/
42
+ .gitignore
43
+
44
+ # Logs
45
+ *.log
46
+ logs/
47
+
48
+ # Runtime data
49
+ pids/
50
+ *.pid
51
+ *.seed
52
+ *.pid.lock
53
+
54
+ # Environment
55
+ .env
56
+ .env.local
57
+ .env.development.local
58
+ .env.test.local
59
+ .env.production.local
60
+
61
+ # Build outputs (will be copied from build stage)
62
+ dist/
63
+
64
+ # Development scripts
65
+ start.sh
66
+
67
+ # Documentation
68
+ README.md
.gitignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ node_modules/
DEPLOYMENT.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingFace Deployment Guide
2
+
3
+ This directory contains the uvify application configured for deployment on HuggingFace Spaces.
4
+
5
+ ## Quick Deploy to HuggingFace Spaces
6
+
7
+ ### Method 1: Direct Upload
8
+
9
+ 1. Create a new HuggingFace Space:
10
+ - Go to https://huggingface.co/new-space
11
+ - Choose "Docker" as the SDK
12
+ - Set visibility as needed
13
+
14
+ 2. Upload files:
15
+ - Copy all files from this directory to your HuggingFace Space repository
16
+ - Commit and push the changes
17
+
18
+ 3. The Space will automatically build and deploy using the Dockerfile
19
+
20
+ ### Method 2: Git Integration
21
+
22
+ 1. Create a new HuggingFace Space with Git integration
23
+ 2. Clone your HuggingFace Space repository locally
24
+ 3. Copy the contents of this directory to the cloned repository
25
+ 4. Commit and push:
26
+ ```bash
27
+ git add .
28
+ git commit -m "Initial uvify deployment"
29
+ git push
30
+ ```
31
+
32
+ ## Files Overview
33
+
34
+ - `Dockerfile` - Multi-stage build configuration for HuggingFace
35
+ - `server.py` - Production FastAPI server that serves both API and frontend
36
+ - `requirements.txt` - Python dependencies
37
+ - `.dockerignore` - Excludes unnecessary files from Docker build
38
+ - Frontend files (`src/`, `package.json`, etc.) - React application
39
+
40
+ ## Architecture
41
+
42
+ The deployment uses a multi-stage Docker build:
43
+
44
+ 1. **Frontend Stage**: Builds the React application using Node.js and Vite
45
+ 2. **Production Stage**:
46
+ - Uses Python 3.10 base image
47
+ - Installs uvify and FastAPI dependencies using `uv`
48
+ - Copies built frontend assets
49
+ - Serves both API and static files on port 7860
50
+
51
+ ## API Endpoints
52
+
53
+ Once deployed, your application will be available at:
54
+ - Frontend: `https://your-space-name.hf.space/`
55
+ - API docs: `https://your-space-name.hf.space/docs`
56
+ - API: `https://your-space-name.hf.space/api/`
57
+
58
+ ## Environment Variables
59
+
60
+ The application uses these environment variables:
61
+ - `PORT` - Server port (default: 7860 for HuggingFace)
62
+
63
+ ## Local Testing
64
+
65
+ To test the Docker build locally:
66
+
67
+ ```bash
68
+ # Build the image
69
+ docker build -t uvify-app .
70
+
71
+ # Run the container
72
+ docker run -p 7860:7860 uvify-app
73
+ ```
74
+
75
+ Then visit http://localhost:7860 to test the application.
76
+
77
+ ## Troubleshooting
78
+
79
+ ### Build Issues
80
+ - Ensure all dependencies are correctly specified in `requirements.txt`
81
+ - Check that the frontend builds successfully with `npm run build`
82
+
83
+ ### Runtime Issues
84
+ - Check the HuggingFace Space logs for Python/FastAPI errors
85
+ - Verify that the uvify package is correctly installed and accessible
86
+
87
+ ### CORS Issues
88
+ - The server.py is configured to allow all origins for HuggingFace deployment
89
+ - If you need to restrict origins, modify the CORS middleware in `server.py`
Dockerfile ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage build for uvify HuggingFace app
2
+ FROM node:20-slim AS frontend-builder
3
+
4
+ # Set working directory for frontend build
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install Node.js dependencies
11
+ RUN npm ci
12
+
13
+ # Copy frontend source code
14
+ COPY src/ ./src/
15
+ COPY public/ ./public/
16
+ COPY index.html ./
17
+ COPY vite.config.ts ./
18
+ COPY tsconfig*.json ./
19
+ COPY tailwind.config.js ./
20
+ COPY postcss.config.js ./
21
+ COPY eslint.config.js ./
22
+
23
+ # Build the frontend for production
24
+ RUN npm run build
25
+
26
+ # Production stage
27
+ FROM python:3.10-slim
28
+
29
+ # Create a non-root user
30
+ RUN useradd --create-home --shell /bin/bash user
31
+
32
+ # Set working directory
33
+ WORKDIR /app
34
+
35
+ # Install system dependencies and uv
36
+ RUN apt-get update && apt-get install -y \
37
+ curl \
38
+ git \
39
+ && rm -rf /var/lib/apt/lists/*
40
+
41
+ # Install uv for fast Python package management
42
+ RUN pip install uv
43
+
44
+ # Copy frontend build from previous stage
45
+ COPY --chown=user --from=frontend-builder /app/dist ./static
46
+
47
+ # Copy Python backend files
48
+ COPY --chown=user server.py ./
49
+ COPY --chown=user requirements.txt ./
50
+
51
+ # Install Python dependencies using uv
52
+ RUN uv pip install --system -r requirements.txt
53
+
54
+ # Change ownership of the app directory to user
55
+ RUN chown -R user:user /app
56
+
57
+ # Switch to non-root user
58
+ USER user
59
+
60
+ # Expose port 7860 (HuggingFace default)
61
+ EXPOSE 7860
62
+
63
+ # Set environment variables
64
+ ENV PORT=7860
65
+ ENV PYTHONPATH=/app
66
+
67
+ # Health check
68
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
69
+ CMD curl -f http://localhost:7860/docs || exit 1
70
+
71
+ # Run the production server
72
+ CMD ["python", "server.py"]
README.md CHANGED
@@ -1,12 +1,111 @@
1
- ---
2
- title: Uvify
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: Python Manager
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Uvify UI
2
+
3
+ A modern web interface for [uvify](https://github.com/avilum/uvify) - Turn Python repositories into uv environment oneliners.
4
+
5
+ ## Features
6
+
7
+ - 🚀 Analyze GitHub repositories and local directories
8
+ - 📦 Parse dependencies from requirements.txt, pyproject.toml, and setup.py
9
+ - 🎯 Generate ready-to-use `uv` commands
10
+ - 📋 Copy commands and download results as JSON
11
+ - 🎨 Beautiful, GitIngest-inspired UI
12
+
13
+ ## Quick Start
14
+
15
+ ### Prerequisites
16
+
17
+ - Node.js 18+
18
+ - Python 3.8+
19
+ - [uv](https://github.com/astral-sh/uv) package manager
20
+
21
+ Install uv if you haven't already:
22
+ ```bash
23
+ curl -LsSf https://astral.sh/uv/install.sh | sh
24
+ # or on macOS:
25
+ brew install uv
26
+ ```
27
+
28
+ ### Installation & Running
29
+
30
+ The easiest way to run both the UI and API servers:
31
+
32
+ ```bash
33
+ npm start
34
+ ```
35
+
36
+ This will:
37
+ 1. Install all dependencies (if needed)
38
+ 2. Start the uvify API server on http://localhost:8000
39
+ 3. Start the UI development server on http://localhost:5173
40
+
41
+ ### Manual Setup
42
+
43
+ If you prefer to run the servers separately:
44
+
45
+ 1. Install dependencies:
46
+ ```bash
47
+ npm install
48
+ uv venv
49
+ source .venv/bin/activate
50
+ uv pip install 'uvify[api]'
51
+ ```
52
+
53
+ 2. Start the API server:
54
+ ```bash
55
+ npm run start:api
56
+ ```
57
+
58
+ 3. In another terminal, start the UI:
59
+ ```bash
60
+ npm run start:ui
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ 1. Enter a GitHub repository URL or username/repo format
66
+ 2. Click "Analyze" to process the repository
67
+ 3. View the results showing:
68
+ - Parsed dependencies per file
69
+ - Ready-to-use `uv` commands
70
+ - Python version requirements
71
+ - Directory structure
72
+
73
+ ## Development
74
+
75
+ ### Project Structure
76
+
77
+ ```
78
+ uvifyUI/
79
+ ├── src/
80
+ │ ├── components/ # React components
81
+ │ ├── services/ # API integration
82
+ │ └── types/ # TypeScript types
83
+ ├── public/ # Static assets
84
+ └── start.sh # Unified start script
85
+ ```
86
+
87
+ ### Available Scripts
88
+
89
+ - `npm start` - Run both UI and API servers
90
+ - `npm run dev` - Run UI development server only
91
+ - `npm run build` - Build for production
92
+ - `npm run lint` - Run ESLint
93
+
94
+ ## Environment Variables
95
+
96
+ Copy `.env.example` to `.env` and configure:
97
+
98
+ ```
99
+ VITE_API_URL=http://localhost:8000
100
+ ```
101
+
102
+ ## Built With
103
+
104
+ - React + TypeScript
105
+ - Vite
106
+ - Tailwind CSS
107
+ - uvify API backend
108
+
109
+ ## License
110
+
111
+ This project is built on top of uvify. See the [uvify repository](https://github.com/avilum/uvify) for license information.
eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { globalIgnores } from 'eslint/config'
7
+
8
+ export default tseslint.config([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs['recommended-latest'],
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Vite + React + TS</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "uvify-ui",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "start": "./start.sh",
12
+ "start:ui": "vite",
13
+ "start:api": "source .venv/bin/activate && python -m uvicorn src.uvify:api --host 0.0.0.0 --port 8000"
14
+ },
15
+ "dependencies": {
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.30.1",
21
+ "@types/react": "^19.1.8",
22
+ "@types/react-dom": "^19.1.6",
23
+ "@vitejs/plugin-react": "^4.6.0",
24
+ "autoprefixer": "^10.4.21",
25
+ "eslint": "^9.30.1",
26
+ "eslint-plugin-react-hooks": "^5.2.0",
27
+ "eslint-plugin-react-refresh": "^0.4.20",
28
+ "globals": "^16.3.0",
29
+ "postcss": "^8.5.6",
30
+ "tailwindcss": "^3.4.17",
31
+ "typescript": "~5.8.3",
32
+ "typescript-eslint": "^8.35.1",
33
+ "vite": "^7.0.4"
34
+ }
35
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
public/vite.svg ADDED
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ uvify[api]
2
+ fastapi
3
+ uvicorn
4
+ python-multipart
server.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Production server for uvify HuggingFace deployment
4
+ Serves both React frontend and FastAPI backend together
5
+ """
6
+ import os
7
+ from fastapi import FastAPI
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi.responses import FileResponse
11
+ import uvify
12
+
13
+ # Create a new FastAPI app that combines everything
14
+ app = FastAPI(
15
+ title="Uvify",
16
+ description="Analyze GitHub repositories to generate uv commands",
17
+ version="1.0.0"
18
+ )
19
+
20
+ # Add CORS middleware for HuggingFace deployment
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=["*"], # Allow all origins for HuggingFace
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+ # Mount the uvify API under /api prefix to avoid conflicts
30
+ app.mount("/api", uvify.api, name="uvify_api")
31
+
32
+ # Mount static files for the built React app
33
+ app.mount("/static", StaticFiles(directory="static"), name="static")
34
+
35
+ # Serve the React app for the root route
36
+ @app.get("/")
37
+ async def serve_index():
38
+ """Serve the React app index page"""
39
+ return FileResponse("static/index.html")
40
+
41
+ # Serve the React app for all other frontend routes
42
+ @app.get("/{path:path}")
43
+ async def serve_frontend(path: str):
44
+ """Serve static files or fallback to React app for client-side routing"""
45
+ # Skip API routes - they're handled by the mounted API
46
+ if path.startswith("api/"):
47
+ return {"error": "API route not found"}
48
+
49
+ # Try to serve static file first (CSS, JS, images, etc.)
50
+ file_path = f"static/{path}"
51
+ if os.path.exists(file_path) and os.path.isfile(file_path):
52
+ return FileResponse(file_path)
53
+
54
+ # Fallback to React app index.html for client-side routing
55
+ return FileResponse("static/index.html")
56
+
57
+ if __name__ == "__main__":
58
+ import uvicorn
59
+ port = int(os.environ.get("PORT", 7860))
60
+ uvicorn.run(app, host="0.0.0.0", port=port)
src/App.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import Header from './components/Header';
3
+ import Hero from './components/Hero';
4
+ import Results from './components/Results';
5
+ import { analyzeRepository } from './services/uvifyApi';
6
+ import type { UvifyResult } from './types/uvify';
7
+
8
+ function App() {
9
+ const [results, setResults] = useState<UvifyResult[]>([]);
10
+ const [isLoading, setIsLoading] = useState(false);
11
+ const [error, setError] = useState<string>('');
12
+ const [currentSource, setCurrentSource] = useState<string>('');
13
+
14
+ const handleAnalyze = async (source: string) => {
15
+ setIsLoading(true);
16
+ setError('');
17
+ setResults([]);
18
+ setCurrentSource(source);
19
+
20
+ try {
21
+ const data = await analyzeRepository(source);
22
+ setResults(data);
23
+ } catch (err) {
24
+ setError(err instanceof Error ? err.message : 'Failed to analyze repository');
25
+ } finally {
26
+ setIsLoading(false);
27
+ }
28
+ };
29
+
30
+ return (
31
+ <div className="min-h-screen bg-gray-900 text-white">
32
+ <Header />
33
+ <Hero onAnalyze={handleAnalyze} isLoading={isLoading} />
34
+ <Results results={results} source={currentSource} error={error} />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export default App
src/assets/react.svg ADDED
src/components/Header.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const Header = () => {
2
+ return (
3
+ <header className="bg-gray-900 border-b border-gray-800">
4
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
5
+ <div className="flex justify-between items-center h-16">
6
+ <h1 className="text-2xl font-bold">
7
+ <a href="/" className="flex items-center gap-1">
8
+ <span className="text-uvify-blue">Uv</span>
9
+ <span className="text-uvify-purple">ify</span>
10
+ </a>
11
+ </h1>
12
+ <nav className="flex items-center gap-6">
13
+ <a
14
+ href="https://github.com/avilum/uvify"
15
+ target="_blank"
16
+ rel="noopener noreferrer"
17
+ className="flex items-center gap-2 text-gray-300 hover:text-white transition-colors"
18
+ >
19
+ <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
20
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
21
+ </svg>
22
+ GitHub
23
+ </a>
24
+ </nav>
25
+ </div>
26
+ </div>
27
+ </header>
28
+ );
29
+ };
30
+
31
+ export default Header;
src/components/Hero.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+
3
+ interface HeroProps {
4
+ onAnalyze: (source: string) => void;
5
+ isLoading: boolean;
6
+ }
7
+
8
+ const Hero = ({ onAnalyze, isLoading }: HeroProps) => {
9
+ const [source, setSource] = useState('');
10
+
11
+ const exampleRepos = [
12
+ { name: 'requests', url: 'psf/requests' },
13
+ { name: 'FastAPI', url: 'fastapi/fastapi' },
14
+ { name: 'Flask', url: 'pallets/flask' },
15
+ { name: 'Black', url: 'psf/black' },
16
+ { name: 'Poetry', url: 'python-poetry/poetry' },
17
+ ];
18
+
19
+ const handleSubmit = (e: React.FormEvent) => {
20
+ e.preventDefault();
21
+ if (source.trim()) {
22
+ onAnalyze(source.trim());
23
+ }
24
+ };
25
+
26
+ const handleExample = (url: string) => {
27
+ setSource(url);
28
+ onAnalyze(url);
29
+ };
30
+
31
+ return (
32
+ <section className="py-12 px-4">
33
+ <div className="max-w-4xl mx-auto">
34
+ <div className="text-center mb-10">
35
+ <div className="flex justify-center items-center gap-4 mb-6">
36
+ <span className="text-6xl">⚡</span>
37
+ <h1 className="text-5xl font-bold">
38
+ <span className="text-gray-100">Python Environment</span>
39
+ <br />
40
+ <span className="bg-gradient-to-r from-uvify-blue to-uvify-purple bg-clip-text text-transparent">
41
+ Simplified
42
+ </span>
43
+ </h1>
44
+ <span className="text-6xl">🐍</span>
45
+ </div>
46
+ <p className="text-xl text-gray-400 mb-2">
47
+ Turn any Python repository into a uv environment oneliner.
48
+ </p>
49
+ <p className="text-lg text-gray-500">
50
+ Analyze dependencies from requirements.txt, pyproject.toml, and setup.py files.
51
+ </p>
52
+ </div>
53
+
54
+ <div className="bg-gray-800 rounded-2xl p-8 shadow-2xl border border-gray-700">
55
+ <form onSubmit={handleSubmit} className="space-y-6">
56
+ <div className="flex gap-3">
57
+ <input
58
+ type="text"
59
+ value={source}
60
+ onChange={(e) => setSource(e.target.value)}
61
+ placeholder="https://github.com/owner/repo or owner/repo"
62
+ className="flex-1 px-4 py-3 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-uvify-blue focus:ring-1 focus:ring-uvify-blue"
63
+ disabled={isLoading}
64
+ />
65
+ <button
66
+ type="submit"
67
+ disabled={isLoading || !source.trim()}
68
+ className="px-8 py-3 bg-gradient-to-r from-uvify-blue to-uvify-purple text-white font-semibold rounded-lg hover:shadow-lg transform hover:-translate-y-0.5 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
69
+ >
70
+ {isLoading ? 'Analyzing...' : 'Analyze'}
71
+ </button>
72
+ </div>
73
+ </form>
74
+
75
+ <div className="mt-6">
76
+ <p className="text-sm text-gray-500 mb-3">Try these example repositories:</p>
77
+ <div className="flex flex-wrap gap-2">
78
+ {exampleRepos.map((repo) => (
79
+ <button
80
+ key={repo.name}
81
+ onClick={() => handleExample(repo.url)}
82
+ className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded-md text-sm transition-colors"
83
+ disabled={isLoading}
84
+ >
85
+ {repo.name}
86
+ </button>
87
+ ))}
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ <p className="text-center text-gray-500 text-sm mt-6">
93
+ Supports GitHub repositories and local directories
94
+ </p>
95
+ </div>
96
+ </section>
97
+ );
98
+ };
99
+
100
+ export default Hero;
src/components/Results.tsx ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import type { UvifyResult } from '../types/uvify';
3
+
4
+ interface ResultsProps {
5
+ results: UvifyResult[];
6
+ source: string;
7
+ error?: string;
8
+ }
9
+
10
+ const Results = ({ results, source, error }: ResultsProps) => {
11
+ const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
12
+
13
+ const copyToClipboard = (text: string, index: number) => {
14
+ navigator.clipboard.writeText(text).then(() => {
15
+ setCopiedIndex(index);
16
+ setTimeout(() => setCopiedIndex(null), 2000);
17
+ });
18
+ };
19
+
20
+ const downloadResults = () => {
21
+ const dataStr = JSON.stringify(results, null, 2);
22
+ const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
23
+
24
+ const exportFileDefaultName = `uvify-${source.replace(/[^a-z0-9]/gi, '-')}.json`;
25
+
26
+ const linkElement = document.createElement('a');
27
+ linkElement.setAttribute('href', dataUri);
28
+ linkElement.setAttribute('download', exportFileDefaultName);
29
+ linkElement.click();
30
+ };
31
+
32
+ if (error) {
33
+ return (
34
+ <div className="max-w-4xl mx-auto px-4 py-8">
35
+ <div className="bg-red-900/20 border border-red-800 rounded-lg p-6">
36
+ <h3 className="text-red-400 font-semibold mb-2">Error</h3>
37
+ <p className="text-gray-300">{error}</p>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ if (results.length === 0) {
44
+ return null;
45
+ }
46
+
47
+ const totalDeps = results.reduce((acc, r) => acc + r.dependencies.length, 0);
48
+ const pythonVersions = [...new Set(results.map(r => r.pythonVersion).filter(Boolean))];
49
+
50
+ return (
51
+ <div className="max-w-6xl mx-auto px-4 py-8">
52
+ <div className="grid md:grid-cols-2 gap-6 mb-8">
53
+ <div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
54
+ <h3 className="text-lg font-semibold mb-4">Repository Info</h3>
55
+ <div className="space-y-2 text-sm">
56
+ <div className="flex justify-between">
57
+ <span className="text-gray-400">Repository:</span>
58
+ <span className="font-mono">{source}</span>
59
+ </div>
60
+ <div className="flex justify-between">
61
+ <span className="text-gray-400">Files analyzed:</span>
62
+ <span>{results.length}</span>
63
+ </div>
64
+ <div className="flex justify-between">
65
+ <span className="text-gray-400">Total dependencies:</span>
66
+ <span>{totalDeps}</span>
67
+ </div>
68
+ {pythonVersions.length > 0 && (
69
+ <div className="flex justify-between">
70
+ <span className="text-gray-400">Python versions:</span>
71
+ <span className="font-mono">{pythonVersions.join(', ')}</span>
72
+ </div>
73
+ )}
74
+ </div>
75
+ <div className="mt-4 flex gap-3">
76
+ <button
77
+ onClick={() => copyToClipboard(JSON.stringify(results, null, 2), -1)}
78
+ className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-sm rounded-md transition-colors flex items-center justify-center gap-2"
79
+ >
80
+ {copiedIndex === -1 ? '✓ Copied!' : '📋 Copy all'}
81
+ </button>
82
+ <button
83
+ onClick={downloadResults}
84
+ className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-sm rounded-md transition-colors flex items-center justify-center gap-2"
85
+ >
86
+ 💾 Download
87
+ </button>
88
+ </div>
89
+ </div>
90
+
91
+ <div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
92
+ <h3 className="text-lg font-semibold mb-4">Directory Structure</h3>
93
+ <div className="font-mono text-sm text-gray-400 space-y-1">
94
+ <div>└── {source.split('/').pop()}/</div>
95
+ {results.map((result, idx) => (
96
+ <div key={idx} className="ml-6">
97
+ {idx === results.length - 1 ? '└── ' : '├── '}
98
+ {result.file}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <h2 className="text-2xl font-bold mb-6">Analysis Results</h2>
106
+
107
+ <div className="space-y-6">
108
+ {results.map((result, index) => (
109
+ <div key={index} className="bg-gray-800 rounded-lg p-6 border border-gray-700">
110
+ <div className="flex items-start justify-between mb-4">
111
+ <div>
112
+ <h3 className="text-xl font-semibold mb-1">
113
+ {result.packageName || result.file}
114
+ </h3>
115
+ <p className="text-sm text-gray-400">
116
+ {result.fileType} • {result.dependencies.length} dependencies
117
+ {result.pythonVersion && ` • Python ${result.pythonVersion}`}
118
+ </p>
119
+ </div>
120
+ <button
121
+ onClick={() => copyToClipboard(result.oneLiner, index)}
122
+ className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-sm rounded-md transition-colors"
123
+ >
124
+ {copiedIndex === index ? '✓' : '📋'}
125
+ </button>
126
+ </div>
127
+
128
+ <div className="space-y-4">
129
+ <div className="bg-gray-900 rounded-lg p-4 overflow-x-auto">
130
+ <p className="text-xs text-gray-500 mb-2">One-liner command:</p>
131
+ <code className="text-sm text-green-400 font-mono whitespace-pre">
132
+ {result.oneLiner}
133
+ </code>
134
+ </div>
135
+
136
+ {result.uvInstallFromSource && (
137
+ <div className="bg-gray-900 rounded-lg p-4 overflow-x-auto">
138
+ <p className="text-xs text-gray-500 mb-2">Install from source:</p>
139
+ <code className="text-sm text-blue-400 font-mono whitespace-pre">
140
+ {result.uvInstallFromSource}
141
+ </code>
142
+ </div>
143
+ )}
144
+
145
+ {result.dependencies.length > 0 && (
146
+ <div>
147
+ <p className="text-sm text-gray-400 mb-2">Dependencies:</p>
148
+ <div className="bg-gray-900 rounded-lg p-4">
149
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
150
+ {result.dependencies.map((dep, depIndex) => (
151
+ <code key={depIndex} className="text-sm text-gray-300 font-mono">
152
+ {dep}
153
+ </code>
154
+ ))}
155
+ </div>
156
+ </div>
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+ ))}
162
+ </div>
163
+ </div>
164
+ );
165
+ };
166
+
167
+ export default Results;
src/index.css ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.tsx'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
src/services/uvifyApi.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { UvifyResult } from '../types/uvify';
2
+
3
+ const API_BASE_URL = import.meta.env.VITE_API_URL || (import.meta.env.PROD ? '' : 'http://localhost:8000');
4
+
5
+ export const analyzeRepository = async (source: string): Promise<UvifyResult[]> => {
6
+ try {
7
+ // Clean up the source input
8
+ let cleanSource = source.trim();
9
+
10
+ // Remove https://github.com/ prefix if present
11
+ if (cleanSource.startsWith('https://github.com/')) {
12
+ cleanSource = cleanSource.replace('https://github.com/', '');
13
+ }
14
+
15
+ // Remove trailing .git if present
16
+ if (cleanSource.endsWith('.git')) {
17
+ cleanSource = cleanSource.slice(0, -4);
18
+ }
19
+
20
+ // Make the API call to the mounted API endpoint
21
+ const response = await fetch(`${API_BASE_URL}/api/${cleanSource}`);
22
+
23
+ if (!response.ok) {
24
+ const errorText = await response.text();
25
+ throw new Error(errorText || `HTTP error! status: ${response.status}`);
26
+ }
27
+
28
+ const data = await response.json();
29
+ return data;
30
+ } catch (error) {
31
+ if (error instanceof Error) {
32
+ throw error;
33
+ }
34
+ throw new Error('An unexpected error occurred');
35
+ }
36
+ };
src/types/uvify.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface UvifyResult {
2
+ file: string;
3
+ fileType: string;
4
+ oneLiner: string;
5
+ uvInstallFromSource?: string;
6
+ dependencies: string[];
7
+ packageName?: string;
8
+ pythonVersion?: string;
9
+ isLocal: boolean;
10
+ }
11
+
12
+ export interface UvifyRequest {
13
+ source: string;
14
+ excludePatterns?: string[];
15
+ includePatterns?: string[];
16
+ }
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
start.sh ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Colors for output
4
+ RED='\033[0;31m'
5
+ GREEN='\033[0;32m'
6
+ YELLOW='\033[1;33m'
7
+ NC='\033[0m' # No Color
8
+
9
+ echo -e "${GREEN}Starting Uvify UI Server...${NC}"
10
+
11
+ # Check if node_modules exists
12
+ if [ ! -d "node_modules" ]; then
13
+ echo -e "${YELLOW}Installing Node dependencies...${NC}"
14
+ npm install
15
+ fi
16
+
17
+ # Activate Python virtual environment
18
+ if [ -d ".venv" ]; then
19
+ source .venv/bin/activate
20
+ else
21
+ echo -e "${YELLOW}Creating Python virtual environment...${NC}"
22
+ uv venv
23
+ source .venv/bin/activate
24
+ echo -e "${YELLOW}Installing Python dependencies...${NC}"
25
+ uv pip install 'uvify[api]'
26
+ fi
27
+
28
+ # Create .env file if it doesn't exist
29
+ if [ ! -f ".env" ]; then
30
+ cp .env.example .env
31
+ echo -e "${YELLOW}Created .env file from .env.example${NC}"
32
+ fi
33
+
34
+ # Function to cleanup on exit
35
+ cleanup() {
36
+ echo -e "\n${YELLOW}Shutting down servers...${NC}"
37
+ kill $UVIFY_PID $VITE_PID 2>/dev/null
38
+ exit
39
+ }
40
+
41
+ trap cleanup EXIT INT TERM
42
+
43
+ # Start uvify backend server
44
+ echo -e "${GREEN}Starting Uvify backend server on http://localhost:8000${NC}"
45
+ # Use our CORS wrapper to handle cross-origin requests
46
+ python uvify_cors_wrapper.py &
47
+ UVIFY_PID=$!
48
+
49
+ # Wait a moment for the backend to start
50
+ sleep 2
51
+
52
+ # Start Vite frontend server
53
+ echo -e "${GREEN}Starting Vite frontend server on http://localhost:5173${NC}"
54
+ npm run dev &
55
+ VITE_PID=$!
56
+
57
+ echo -e "${GREEN}Both servers are running!${NC}"
58
+ echo -e "Frontend: ${GREEN}http://localhost:5173${NC}"
59
+ echo -e "Backend API: ${GREEN}http://localhost:8000${NC}"
60
+ echo -e "\nPress ${YELLOW}Ctrl+C${NC} to stop both servers"
61
+
62
+ # Wait for both processes
63
+ wait $UVIFY_PID $VITE_PID
tailwind.config.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ 'uvify-blue': '#3B82F6',
11
+ 'uvify-purple': '#8B5CF6',
12
+ },
13
+ fontFamily: {
14
+ 'mono': ['Consolas', 'Monaco', 'Courier New', 'monospace'],
15
+ },
16
+ },
17
+ },
18
+ plugins: [],
19
+ }
tsconfig.app.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "erasableSyntaxOnly": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true
25
+ },
26
+ "include": ["src"]
27
+ }
tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true
23
+ },
24
+ "include": ["vite.config.ts"]
25
+ }
uvify_cors_wrapper.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ CORS wrapper for uvify API
4
+ """
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ import uvify
8
+
9
+ # Get the original FastAPI app from uvify
10
+ app = uvify.api
11
+
12
+ # Add CORS middleware
13
+ app.add_middleware(
14
+ CORSMiddleware,
15
+ allow_origins=["http://localhost:5173", "http://localhost:5174", "http://localhost:5175"],
16
+ allow_credentials=True,
17
+ allow_methods=["*"],
18
+ allow_headers=["*"],
19
+ )
20
+
21
+ if __name__ == "__main__":
22
+ import uvicorn
23
+ uvicorn.run(app, host="0.0.0.0", port=8000)
vite.config.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ server: {
8
+ proxy: {
9
+ '/api': {
10
+ target: 'http://localhost:8000',
11
+ changeOrigin: true,
12
+ }
13
+ }
14
+ },
15
+ build: {
16
+ outDir: 'dist',
17
+ assetsDir: 'assets',
18
+ }
19
+ })