diff --git a/server/database/db.js b/server/database/db.js
index 335e387..dbb9d37 100644
--- a/server/database/db.js
+++ b/server/database/db.js
@@ -55,12 +55,40 @@ if (process.env.DATABASE_PATH) {
console.log(c.dim('═'.repeat(60)));
console.log('');
+const runMigrations = () => {
+ try {
+ const tableInfo = db.prepare("PRAGMA table_info(users)").all();
+ const columnNames = tableInfo.map(col => col.name);
+
+ if (!columnNames.includes('git_name')) {
+ console.log('Running migration: Adding git_name column');
+ db.exec('ALTER TABLE users ADD COLUMN git_name TEXT');
+ }
+
+ if (!columnNames.includes('git_email')) {
+ console.log('Running migration: Adding git_email column');
+ db.exec('ALTER TABLE users ADD COLUMN git_email TEXT');
+ }
+
+ if (!columnNames.includes('has_completed_onboarding')) {
+ console.log('Running migration: Adding has_completed_onboarding column');
+ db.exec('ALTER TABLE users ADD COLUMN has_completed_onboarding BOOLEAN DEFAULT 0');
+ }
+
+ console.log('Database migrations completed successfully');
+ } catch (error) {
+ console.error('Error running migrations:', error.message);
+ throw error;
+ }
+};
+
// Initialize database with schema
const initializeDatabase = async () => {
try {
const initSQL = fs.readFileSync(INIT_SQL_PATH, 'utf8');
db.exec(initSQL);
console.log('Database initialized successfully');
+ runMigrations();
} catch (error) {
console.error('Error initializing database:', error.message);
throw error;
@@ -126,6 +154,42 @@ const userDb = {
} catch (err) {
throw err;
}
+ },
+
+ updateGitConfig: (userId, gitName, gitEmail) => {
+ try {
+ const stmt = db.prepare('UPDATE users SET git_name = ?, git_email = ? WHERE id = ?');
+ stmt.run(gitName, gitEmail, userId);
+ } catch (err) {
+ throw err;
+ }
+ },
+
+ getGitConfig: (userId) => {
+ try {
+ const row = db.prepare('SELECT git_name, git_email FROM users WHERE id = ?').get(userId);
+ return row;
+ } catch (err) {
+ throw err;
+ }
+ },
+
+ completeOnboarding: (userId) => {
+ try {
+ const stmt = db.prepare('UPDATE users SET has_completed_onboarding = 1 WHERE id = ?');
+ stmt.run(userId);
+ } catch (err) {
+ throw err;
+ }
+ },
+
+ hasCompletedOnboarding: (userId) => {
+ try {
+ const row = db.prepare('SELECT has_completed_onboarding FROM users WHERE id = ?').get(userId);
+ return row?.has_completed_onboarding === 1;
+ } catch (err) {
+ throw err;
+ }
}
};
diff --git a/server/database/init.sql b/server/database/init.sql
index 5304481..e52daef 100644
--- a/server/database/init.sql
+++ b/server/database/init.sql
@@ -8,7 +8,10 @@ CREATE TABLE IF NOT EXISTS users (
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME,
- is_active BOOLEAN DEFAULT 1
+ is_active BOOLEAN DEFAULT 1,
+ git_name TEXT,
+ git_email TEXT,
+ has_completed_onboarding BOOLEAN DEFAULT 0
);
-- Indexes for performance
diff --git a/server/index.js b/server/index.js
index 0514346..a094d9d 100755
--- a/server/index.js
+++ b/server/index.js
@@ -71,6 +71,7 @@ import settingsRoutes from './routes/settings.js';
import agentRoutes from './routes/agent.js';
import projectsRoutes from './routes/projects.js';
import cliAuthRoutes from './routes/cli-auth.js';
+import userRoutes from './routes/user.js';
import { initializeDatabase } from './database/db.js';
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
@@ -254,6 +255,9 @@ app.use('/api/settings', authenticateToken, settingsRoutes);
// CLI Authentication API Routes (protected)
app.use('/api/cli', authenticateToken, cliAuthRoutes);
+// User API Routes (protected)
+app.use('/api/user', authenticateToken, userRoutes);
+
// Agent API Routes (uses API key authentication)
app.use('/api/agent', agentRoutes);
diff --git a/server/routes/user.js b/server/routes/user.js
new file mode 100644
index 0000000..ef54078
--- /dev/null
+++ b/server/routes/user.js
@@ -0,0 +1,93 @@
+import express from 'express';
+import { userDb } from '../database/db.js';
+import { authenticateToken } from '../middleware/auth.js';
+import { exec } from 'child_process';
+import { promisify } from 'util';
+
+const execAsync = promisify(exec);
+const router = express.Router();
+
+router.get('/git-config', authenticateToken, async (req, res) => {
+ try {
+ const userId = req.user.id;
+ const gitConfig = userDb.getGitConfig(userId);
+
+ res.json({
+ success: true,
+ gitName: gitConfig?.git_name || null,
+ gitEmail: gitConfig?.git_email || null
+ });
+ } catch (error) {
+ console.error('Error getting git config:', error);
+ res.status(500).json({ error: 'Failed to get git configuration' });
+ }
+});
+
+// Apply git config globally via git config --global
+router.post('/git-config', authenticateToken, async (req, res) => {
+ try {
+ const userId = req.user.id;
+ const { gitName, gitEmail } = req.body;
+
+ if (!gitName || !gitEmail) {
+ return res.status(400).json({ error: 'Git name and email are required' });
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(gitEmail)) {
+ return res.status(400).json({ error: 'Invalid email format' });
+ }
+
+ userDb.updateGitConfig(userId, gitName, gitEmail);
+
+ try {
+ await execAsync(`git config --global user.name "${gitName.replace(/"/g, '\\"')}"`);
+ await execAsync(`git config --global user.email "${gitEmail.replace(/"/g, '\\"')}"`);
+ console.log(`Applied git config globally: ${gitName} <${gitEmail}>`);
+ } catch (gitError) {
+ console.error('Error applying git config:', gitError);
+ }
+
+ res.json({
+ success: true,
+ gitName,
+ gitEmail
+ });
+ } catch (error) {
+ console.error('Error updating git config:', error);
+ res.status(500).json({ error: 'Failed to update git configuration' });
+ }
+});
+
+router.post('/complete-onboarding', authenticateToken, async (req, res) => {
+ try {
+ const userId = req.user.id;
+ userDb.completeOnboarding(userId);
+
+ res.json({
+ success: true,
+ message: 'Onboarding completed successfully'
+ });
+ } catch (error) {
+ console.error('Error completing onboarding:', error);
+ res.status(500).json({ error: 'Failed to complete onboarding' });
+ }
+});
+
+router.get('/onboarding-status', authenticateToken, async (req, res) => {
+ try {
+ const userId = req.user.id;
+ const hasCompleted = userDb.hasCompletedOnboarding(userId);
+
+ res.json({
+ success: true,
+ hasCompletedOnboarding: hasCompleted
+ });
+ } catch (error) {
+ console.error('Error checking onboarding status:', error);
+ res.status(500).json({ error: 'Failed to check onboarding status' });
+ }
+});
+
+export default router;
diff --git a/src/components/GitSettings.jsx b/src/components/GitSettings.jsx
new file mode 100644
index 0000000..91b0902
--- /dev/null
+++ b/src/components/GitSettings.jsx
@@ -0,0 +1,129 @@
+import { useState, useEffect } from 'react';
+import { Button } from './ui/button';
+import { Input } from './ui/input';
+import { GitBranch, Check } from 'lucide-react';
+import { authenticatedFetch } from '../utils/api';
+
+function GitSettings() {
+ const [gitName, setGitName] = useState('');
+ const [gitEmail, setGitEmail] = useState('');
+ const [gitConfigLoading, setGitConfigLoading] = useState(false);
+ const [gitConfigSaving, setGitConfigSaving] = useState(false);
+ const [saveStatus, setSaveStatus] = useState(null);
+
+ useEffect(() => {
+ loadGitConfig();
+ }, []);
+
+ const loadGitConfig = async () => {
+ try {
+ setGitConfigLoading(true);
+ const response = await authenticatedFetch('/api/user/git-config');
+ if (response.ok) {
+ const data = await response.json();
+ setGitName(data.gitName || '');
+ setGitEmail(data.gitEmail || '');
+ }
+ } catch (error) {
+ console.error('Error loading git config:', error);
+ } finally {
+ setGitConfigLoading(false);
+ }
+ };
+
+ const saveGitConfig = async () => {
+ try {
+ setGitConfigSaving(true);
+ const response = await authenticatedFetch('/api/user/git-config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ gitName, gitEmail })
+ });
+
+ if (response.ok) {
+ setSaveStatus('success');
+ setTimeout(() => setSaveStatus(null), 3000);
+ } else {
+ const data = await response.json();
+ setSaveStatus('error');
+ console.error('Failed to save git config:', data.error);
+ }
+ } catch (error) {
+ console.error('Error saving git config:', error);
+ setSaveStatus('error');
+ } finally {
+ setGitConfigSaving(false);
+ }
+ };
+
+ return (
+
+
+
+
+
Git Configuration
+
+
+
+ Configure your git identity for commits. These settings will be applied globally via git config --global
+
+
+
+
+
+
setGitName(e.target.value)}
+ placeholder="John Doe"
+ disabled={gitConfigLoading}
+ className="w-full"
+ />
+
+ Your name for git commits
+
+
+
+
+
+
setGitEmail(e.target.value)}
+ placeholder="john@example.com"
+ disabled={gitConfigLoading}
+ className="w-full"
+ />
+
+ Your email for git commits
+
+
+
+
+
+
+ {saveStatus === 'success' && (
+
+
+ Saved successfully
+
+ )}
+
+
+
+
+ );
+}
+
+export default GitSettings;
diff --git a/src/components/Onboarding.jsx b/src/components/Onboarding.jsx
new file mode 100644
index 0000000..69dfdd8
--- /dev/null
+++ b/src/components/Onboarding.jsx
@@ -0,0 +1,570 @@
+import React, { useState, useEffect } from 'react';
+import { ChevronRight, ChevronLeft, Check, GitBranch, User, Mail, LogIn, ExternalLink, Loader2 } from 'lucide-react';
+import ClaudeLogo from './ClaudeLogo';
+import CursorLogo from './CursorLogo';
+import LoginModal from './LoginModal';
+import { authenticatedFetch } from '../utils/api';
+import { useAuth } from '../contexts/AuthContext';
+
+const Onboarding = ({ onComplete }) => {
+ const [currentStep, setCurrentStep] = useState(0);
+ const [gitName, setGitName] = useState('');
+ const [gitEmail, setGitEmail] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState('');
+
+ // CLI authentication states
+ const [showLoginModal, setShowLoginModal] = useState(false);
+ const [loginProvider, setLoginProvider] = useState('');
+ const [selectedProject, setSelectedProject] = useState({ name: 'default', fullPath: process.cwd() });
+
+ const [claudeAuthStatus, setClaudeAuthStatus] = useState({
+ authenticated: false,
+ email: null,
+ loading: true,
+ error: null
+ });
+
+ const [cursorAuthStatus, setCursorAuthStatus] = useState({
+ authenticated: false,
+ email: null,
+ loading: true,
+ error: null
+ });
+
+ const { user } = useAuth();
+
+ // Check authentication status on mount and when modal closes
+ useEffect(() => {
+ checkClaudeAuthStatus();
+ checkCursorAuthStatus();
+ }, []);
+
+ // Auto-check authentication status periodically when on CLI steps
+ useEffect(() => {
+ if (currentStep === 1 || currentStep === 2) {
+ const interval = setInterval(() => {
+ if (currentStep === 1) {
+ checkClaudeAuthStatus();
+ } else if (currentStep === 2) {
+ checkCursorAuthStatus();
+ }
+ }, 3000); // Check every 3 seconds
+
+ return () => clearInterval(interval);
+ }
+ }, [currentStep]);
+
+ const checkClaudeAuthStatus = async () => {
+ try {
+ const response = await authenticatedFetch('/api/cli/claude/status');
+ if (response.ok) {
+ const data = await response.json();
+ setClaudeAuthStatus({
+ authenticated: data.authenticated,
+ email: data.email,
+ loading: false,
+ error: data.error || null
+ });
+ } else {
+ setClaudeAuthStatus({
+ authenticated: false,
+ email: null,
+ loading: false,
+ error: 'Failed to check authentication status'
+ });
+ }
+ } catch (error) {
+ console.error('Error checking Claude auth status:', error);
+ setClaudeAuthStatus({
+ authenticated: false,
+ email: null,
+ loading: false,
+ error: error.message
+ });
+ }
+ };
+
+ const checkCursorAuthStatus = async () => {
+ try {
+ const response = await authenticatedFetch('/api/cli/cursor/status');
+ if (response.ok) {
+ const data = await response.json();
+ setCursorAuthStatus({
+ authenticated: data.authenticated,
+ email: data.email,
+ loading: false,
+ error: data.error || null
+ });
+ } else {
+ setCursorAuthStatus({
+ authenticated: false,
+ email: null,
+ loading: false,
+ error: 'Failed to check authentication status'
+ });
+ }
+ } catch (error) {
+ console.error('Error checking Cursor auth status:', error);
+ setCursorAuthStatus({
+ authenticated: false,
+ email: null,
+ loading: false,
+ error: error.message
+ });
+ }
+ };
+
+ const handleClaudeLogin = () => {
+ setLoginProvider('claude');
+ setShowLoginModal(true);
+ };
+
+ const handleCursorLogin = () => {
+ setLoginProvider('cursor');
+ setShowLoginModal(true);
+ };
+
+ const handleLoginComplete = (exitCode) => {
+ if (exitCode === 0) {
+ if (loginProvider === 'claude') {
+ checkClaudeAuthStatus();
+ } else if (loginProvider === 'cursor') {
+ checkCursorAuthStatus();
+ }
+ }
+ };
+
+ const handleNextStep = async () => {
+ setError('');
+
+ // Step 0: Git config validation and submission
+ if (currentStep === 0) {
+ if (!gitName.trim() || !gitEmail.trim()) {
+ setError('Both git name and email are required');
+ return;
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(gitEmail)) {
+ setError('Please enter a valid email address');
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ // Save git config to backend (which will also apply git config --global)
+ const response = await authenticatedFetch('/api/user/git-config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ gitName, gitEmail })
+ });
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Failed to save git configuration');
+ }
+
+ setCurrentStep(currentStep + 1);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ return;
+ }
+
+ // Other steps: just move forward
+ setCurrentStep(currentStep + 1);
+ };
+
+ const handlePrevStep = () => {
+ setError('');
+ setCurrentStep(currentStep - 1);
+ };
+
+ const handleFinish = async () => {
+ setIsSubmitting(true);
+ setError('');
+
+ try {
+ // Mark onboarding as complete
+ const response = await authenticatedFetch('/api/user/complete-onboarding', {
+ method: 'POST'
+ });
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Failed to complete onboarding');
+ }
+
+ // Call the onComplete callback
+ if (onComplete) {
+ onComplete();
+ }
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const steps = [
+ {
+ title: 'Git Configuration',
+ description: 'Set up your git identity for commits',
+ icon: GitBranch,
+ required: true
+ },
+ {
+ title: 'Claude Code CLI',
+ description: 'Connect your Claude Code account',
+ icon: () => ,
+ required: false
+ },
+ {
+ title: 'Cursor CLI',
+ description: 'Connect your Cursor account',
+ icon: () => ,
+ required: false
+ }
+ ];
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 0:
+ return (
+
+
+
+
+
+
Git Configuration
+
+ Configure your git identity to ensure proper attribution for your commits
+
+
+
+
+
+
+
setGitName(e.target.value)}
+ className="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ placeholder="John Doe"
+ required
+ disabled={isSubmitting}
+ />
+
+ This will be used as: git config --global user.name
+
+
+
+
+
+
setGitEmail(e.target.value)}
+ className="w-full px-4 py-3 border border-border rounded-lg bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ placeholder="john@example.com"
+ required
+ disabled={isSubmitting}
+ />
+
+ This will be used as: git config --global user.email
+
+
+
+
+ );
+
+ case 1:
+ return (
+
+
+
+
+
+
Claude Code CLI
+
+ Connect your Claude account to enable AI-powered coding features
+
+
+
+ {/* Auth Status Card */}
+
+
+
+
+
+ {claudeAuthStatus.loading ? 'Checking...' :
+ claudeAuthStatus.authenticated ? 'Connected' : 'Not Connected'}
+
+
+ {claudeAuthStatus.authenticated && (
+
+ )}
+
+
+ {claudeAuthStatus.authenticated && claudeAuthStatus.email && (
+
+ Signed in as: {claudeAuthStatus.email}
+
+ )}
+
+ {!claudeAuthStatus.authenticated && (
+ <>
+
+ Click the button below to authenticate with Claude Code CLI. A terminal will open with authentication instructions.
+
+
+
+ Or manually run: claude auth login
+
+ >
+ )}
+
+ {claudeAuthStatus.error && !claudeAuthStatus.authenticated && (
+
+
{claudeAuthStatus.error}
+
+ )}
+
+
+
+
This step is optional. You can skip and configure it later in Settings.
+
+
+ );
+
+ case 2:
+ return (
+
+
+
+
+
+
Cursor CLI
+
+ Connect your Cursor account to enable AI-powered features
+
+
+
+ {/* Auth Status Card */}
+
+
+
+
+
+ {cursorAuthStatus.loading ? 'Checking...' :
+ cursorAuthStatus.authenticated ? 'Connected' : 'Not Connected'}
+
+
+ {cursorAuthStatus.authenticated && (
+
+ )}
+
+
+ {cursorAuthStatus.authenticated && cursorAuthStatus.email && (
+
+ Signed in as: {cursorAuthStatus.email}
+
+ )}
+
+ {!cursorAuthStatus.authenticated && (
+ <>
+
+ Click the button below to authenticate with Cursor CLI. A terminal will open with authentication instructions.
+
+
+
+ Or manually run: cursor auth login
+
+ >
+ )}
+
+ {cursorAuthStatus.error && !cursorAuthStatus.authenticated && (
+
+
{cursorAuthStatus.error}
+
+ )}
+
+
+
+
This step is optional. You can skip and configure it later in Settings.
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ const isStepValid = () => {
+ switch (currentStep) {
+ case 0:
+ return gitName.trim() && gitEmail.trim() && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(gitEmail);
+ case 1:
+ case 2:
+ return true; // CLI steps are optional
+ default:
+ return false;
+ }
+ };
+
+ return (
+ <>
+
+
+ {/* Progress Steps */}
+
+
+ {steps.map((step, index) => (
+
+
+
+ {index < currentStep ? (
+
+ ) : typeof step.icon === 'function' ? (
+
+ ) : (
+
+ )}
+
+
+
+ {step.title}
+
+ {step.required && (
+
Required
+ )}
+
+
+ {index < steps.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+ {/* Main Card */}
+
+ {renderStepContent()}
+
+ {/* Error Message */}
+ {error && (
+
+ )}
+
+ {/* Navigation Buttons */}
+
+
+
+
+ {currentStep < steps.length - 1 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {/* Login Modal */}
+ {showLoginModal && (
+ setShowLoginModal(false)}
+ provider={loginProvider}
+ project={selectedProject}
+ onLoginComplete={handleLoginComplete}
+ />
+ )}
+ >
+ );
+};
+
+export default Onboarding;
diff --git a/src/components/ProtectedRoute.jsx b/src/components/ProtectedRoute.jsx
index 16419d0..56343ae 100644
--- a/src/components/ProtectedRoute.jsx
+++ b/src/components/ProtectedRoute.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import { useAuth } from '../contexts/AuthContext';
import SetupForm from './SetupForm';
import LoginForm from './LoginForm';
+import Onboarding from './Onboarding';
import { MessageSquare } from 'lucide-react';
const LoadingScreen = () => (
@@ -24,14 +25,20 @@ const LoadingScreen = () => (
);
const ProtectedRoute = ({ children }) => {
- const { user, isLoading, needsSetup } = useAuth();
+ const { user, isLoading, needsSetup, hasCompletedOnboarding, refreshOnboardingStatus } = useAuth();
- // Platform mode: skip all auth UI and directly render children
if (import.meta.env.VITE_IS_PLATFORM === 'true') {
+ if (isLoading) {
+ return ;
+ }
+
+ if (!hasCompletedOnboarding) {
+ return ;
+ }
+
return children;
}
- // Normal OSS mode: standard auth flow
if (isLoading) {
return ;
}
@@ -44,6 +51,10 @@ const ProtectedRoute = ({ children }) => {
return ;
}
+ if (!hasCompletedOnboarding) {
+ return ;
+ }
+
return children;
};
diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx
index d474f98..60fe047 100644
--- a/src/components/Settings.jsx
+++ b/src/components/Settings.jsx
@@ -2,12 +2,13 @@ import { useState, useEffect } from 'react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
-import { X, Plus, Settings as SettingsIcon, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen, LogIn, Key } from 'lucide-react';
+import { X, Plus, Settings as SettingsIcon, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen, LogIn, Key, GitBranch, Check } from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { useTasksSettings } from '../contexts/TasksSettingsContext';
import ClaudeLogo from './ClaudeLogo';
import CursorLogo from './CursorLogo';
import CredentialsSettings from './CredentialsSettings';
+import GitSettings from './GitSettings';
import LoginModal from './LoginModal';
import { authenticatedFetch } from '../utils/api';
@@ -387,7 +388,6 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
await fetchCursorMcpServers();
} catch (error) {
console.error('Error loading tool settings:', error);
- // Set defaults on error
setAllowedTools([]);
setDisallowedTools([]);
setSkipPermissions(false);
@@ -743,6 +743,17 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'tools' }) {
>
Appearance
+