diff --git a/action.yml b/action.yml index a838317..27a4aa8 100644 --- a/action.yml +++ b/action.yml @@ -171,6 +171,10 @@ runs: # Debug output for branch detection echo "Detected branch: $current_branch (event: ${{ github.event_name }})" + + # URL encode docker image for passing to UI + encoded_docker_image=$(url_encode "${{ inputs.image }}") + # Build failed tests array for URL parameters failed_tests_params="" if [ ${#failed_tests[@]} -ne 0 ] && [ -n "${{ inputs.ui_host }}" ]; then @@ -212,7 +216,7 @@ runs: for key in "${!failed_tests[@]}"; do encoded_test_path=$(url_encode "$key") encoded_branch=$(url_encode "$current_branch") - edit_url="${{ inputs.ui_host }}/?test_path=$encoded_test_path&branch=$encoded_branch&$failed_tests_params" + edit_url="${{ inputs.ui_host }}/?test_path=$encoded_test_path&branch=$encoded_branch&docker_image=$encoded_docker_image&$failed_tests_params" echo "- [Edit $key]($edit_url)" >> $GITHUB_STEP_SUMMARY done echo >> $GITHUB_STEP_SUMMARY diff --git a/ui/API.md b/ui/API.md new file mode 100644 index 0000000..c2ad045 --- /dev/null +++ b/ui/API.md @@ -0,0 +1,323 @@ +# API Endpoints Reference + +## Configuration + +### GET /api/config + +Returns application configuration including default docker image. + +**Authentication:** Required (session-based) + +**Request:** +```bash +curl -X GET http://localhost:3000/api/config \ + --cookie "connect.sid=..." \ + -H "Content-Type: application/json" +``` + +**Response:** +```json +{ + "dockerImage": "ghcr.io/manticoresoftware/manticoresearch:test-kit-latest" +} +``` + +**Status Codes:** +- `200 OK` - Configuration returned successfully +- `401 Unauthorized` - Not authenticated + +**Environment Variables:** +- `DOCKER_IMAGE` - Default docker image (optional, falls back to hardcoded default) + +**Usage in Frontend:** +```javascript +const response = await fetch(`${API_URL}/api/config`, { + credentials: 'include' +}); +const config = await response.json(); +console.log(config.dockerImage); // Default docker image +``` + +**Added:** 2025-01-11 +**Location:** `ui/routes.js` +**Related:** See [DOCKER_IMAGE.md](./DOCKER_IMAGE.md) for full docker image configuration details + +## Health Check + +### GET /api/health + +Health check endpoint for monitoring and authentication verification. + +**Authentication:** Required + +**Response:** +```json +{ + "status": "ok", + "authenticated": true, + "user": "username" +} +``` + +## File Operations + +### GET /api/get-file-tree + +Returns hierarchical file tree structure. + +**Authentication:** Required + +**Response:** +```json +{ + "fileTree": [ + { + "name": "test.rec", + "path": "tests/test.rec", + "isDirectory": false + } + ] +} +``` + +### GET /api/get-file + +Retrieves file content with optional WASM parsing for .rec files. + +**Authentication:** Required + +**Query Parameters:** +- `path` (required) - File path relative to test directory + +**Response:** +```json +{ + "content": "raw file content", + "structuredData": { /* parsed test structure */ }, + "wasmparsed": true +} +``` + +### POST /api/save-file + +Saves file content. + +**Authentication:** Required + +**Request Body:** +```json +{ + "path": "tests/test.rec", + "content": "file content", + "structuredData": { /* optional structured data */ } +} +``` + +### POST /api/move-file + +Moves or renames file/directory. + +**Authentication:** Required + +**Request Body:** +```json +{ + "sourcePath": "tests/old.rec", + "targetPath": "tests/new.rec" +} +``` + +### DELETE /api/delete-file + +Deletes file or directory. + +**Authentication:** Required + +**Request Body:** +```json +{ + "path": "tests/test.rec" +} +``` + +## Test Execution + +### POST /api/start-test + +Starts test execution in Docker container. + +**Authentication:** Required + +**Request Body:** +```json +{ + "filePath": "tests/test.rec", + "dockerImage": "ghcr.io/manticoresoftware/manticoresearch:test-kit-latest" +} +``` + +**Response:** +```json +{ + "jobId": "uuid", + "timeout": 30000 +} +``` + +### GET /api/poll-test/:jobId + +Polls test execution status. + +**Authentication:** Required + +**Response:** +```json +{ + "running": false, + "finished": true, + "exitCode": 0, + "success": true, + "testStructure": { /* enriched with results */ } +} +``` + +### POST /api/stop-test/:jobId + +Stops running test. + +**Authentication:** Required + +**Response:** +```json +{ + "message": "Test stopped successfully" +} +``` + +## Git Operations + +### GET /api/git-status + +Returns current git repository status. + +**Authentication:** Required + +**Response:** +```json +{ + "currentBranch": "main", + "hasChanges": false, + "isPrBranch": false, + "repoUrl": "https://github.com/user/repo" +} +``` + +### GET /api/current-branch + +Returns current branch information. + +**Authentication:** Required + +**Response:** +```json +{ + "currentBranch": "main", + "defaultBranch": "master" +} +``` + +### POST /api/reset-to-branch + +Resets repository to specified branch. + +**Authentication:** Required + +**Request Body:** +```json +{ + "branch": "main" +} +``` + +## Authentication + +### GET /auth/github + +Initiates GitHub OAuth flow. + +**Redirect:** GitHub OAuth authorization page + +### GET /auth/github/callback + +GitHub OAuth callback handler. + +**Redirect:** Frontend URL with success/failure + +### GET /api/current-user + +Returns current authenticated user. + +**Response:** +```json +{ + "username": "user", + "displayName": "User Name", + "avatarUrl": "https://..." +} +``` + +### GET /logout + +Logs out current user. + +**Redirect:** Frontend URL + +## Interactive Sessions (Ask AI) + +### POST /api/interactive/start + +Starts interactive AI session. + +**Authentication:** Required + +**Request Body:** +```json +{ + "sessionName": "optional-name" +} +``` + +### POST /api/interactive/:sessionId/input + +Sends input to interactive session. + +**Authentication:** Required + +**Request Body:** +```json +{ + "input": "user input text" +} +``` + +### POST /api/interactive/:sessionId/stop + +Stops interactive session. + +**Authentication:** Required + +### GET /api/interactive/sessions + +Lists available session logs. + +**Authentication:** Required + +### GET /api/interactive/session/:sessionId + +Retrieves session log content. + +**Authentication:** Required + +--- + +**Note:** All endpoints require authentication via session cookies except the OAuth flow endpoints. diff --git a/ui/DOCKER_IMAGE.md b/ui/DOCKER_IMAGE.md new file mode 100644 index 0000000..7ce0322 --- /dev/null +++ b/ui/DOCKER_IMAGE.md @@ -0,0 +1,282 @@ +# Docker Image Configuration + +## Overview + +The CLT UI supports flexible Docker image configuration with a priority-based system that allows defaults, file-specific images, URL parameters, and user overrides. + +## Priority System + +The docker image is determined by the following priority (highest to lowest): + +1. **User Input** - Manual entry in the UI input field (highest priority) +2. **URL Parameter** - `?docker_image=custom:tag` in the URL +3. **File Metadata** - `Docker image: ` in the test file +4. **Environment Variable** - `DOCKER_IMAGE` env variable +5. **Hardcoded Default** - `ghcr.io/manticoresoftware/manticoresearch:test-kit-latest` + +## Configuration Methods + +### 1. Environment Variable (Recommended) + +Set the default docker image via environment variable: + +```bash +# In .env file or environment +DOCKER_IMAGE=ghcr.io/myorg/myimage:latest +``` + +The backend exposes this via `/api/config` endpoint: + +```javascript +GET /api/config +Response: { "dockerImage": "ghcr.io/myorg/myimage:latest" } +``` + +### 2. File Metadata + +Add docker image specification in your `.rec` test file: + +``` +Docker image: custom-image:tag + +––– input ––– +echo "test" +––– output ––– +test +``` + +**Rules:** +- Case-insensitive: `docker image:`, `Docker Image:`, `DOCKER IMAGE:` all work +- Must appear before first `––– input –––` marker +- Can be in description field (if parsed) or raw content +- Extracted via regex: `/docker\s+image:\s*(.+)/i` + +### 3. URL Parameter + +Pass docker image via URL when linking to tests: + +``` +http://localhost:9151/?docker_image=custom:tag +http://localhost:9151/?test_path=test.rec&docker_image=custom:tag +``` + +**Use case:** GitHub Actions can pass custom images when linking to failed tests. + +### 4. User Input + +Users can manually type or clear the docker image in the UI header input field: + +- **Empty input** = Use file metadata or default (placeholder shows default) +- **Typed value** = Override everything (highest priority) +- **Clear input** = Remove override, fall back to file/default + +## Implementation Details + +### Backend (`ui/routes.js`) + +```javascript +// Config endpoint returns default docker image +app.get('/api/config', isAuthenticated, (req, res) => { + return res.json({ + dockerImage: process.env.DOCKER_IMAGE || 'ghcr.io/manticoresoftware/manticoresearch:test-kit-latest' + }); +}); +``` + +### Store (`ui/src/stores/filesStore.ts`) + +```typescript +// Default docker image (mutable, updated from backend) +let DEFAULT_DOCKER_IMAGE = 'ghcr.io/manticoresoftware/manticoresearch:test-kit-latest'; + +interface FilesState { + dockerImage: string; // Effective image used for running tests + userDockerImage: string | null; // User's explicit input (null = no override) + currentFile: RecordingFile | null; +} + +interface RecordingFile { + fileDockerImage?: string | null; // Docker image from file metadata +} + +// Extract docker image from file content +const extractDockerImageFromFile = (testStructure, rawContent) => { + // Search in description or raw content before first ––– input ––– + const match = content.match(/docker\s+image:\s*(.+)/i); + return match ? match[1].trim() : null; +}; + +// Get effective docker image based on priority +const getEffectiveDockerImage = (state: FilesState): string => { + if (state.userDockerImage && state.userDockerImage.trim() !== '') { + return state.userDockerImage; // User override + } + if (state.currentFile?.fileDockerImage) { + return state.currentFile.fileDockerImage; // File metadata + } + return DEFAULT_DOCKER_IMAGE; // Default from env or hardcoded +}; +``` + +### Header Component (`ui/src/components/Header.svelte`) + +```svelte + + + +``` + +### URL Parameter Handling (`ui/src/components/FileExplorer.svelte`) + +```javascript +async function handleUrlChange() { + const urlParams = parseUrlParams(); + + // Set docker image from URL parameter + if (urlParams.dockerImage) { + filesStore.setDockerImage(urlParams.dockerImage); + } +} +``` + +## User Experience + +### Empty Input Field +- **What user sees:** Empty input with placeholder showing default image +- **What happens:** Tests run with file metadata image or default +- **Benefit:** Clean UI, clear indication of what will be used + +### URL Parameter +- **What user sees:** Input field populated with URL parameter value +- **What happens:** Tests run with URL-specified image +- **Benefit:** GitHub Actions can specify custom images for CI runs + +### File Metadata +- **What user sees:** Input field shows file's docker image +- **What happens:** Tests run with file-specified image +- **Benefit:** Per-test customization without manual input + +### User Override +- **What user sees:** Their typed value in input field +- **What happens:** Tests run with user-specified image +- **Benefit:** Quick testing with different images + +## Testing Scenarios + +1. **Fresh load, no file** + - Input: Empty + - Placeholder: Default from env + - Runs with: Default + +2. **Load file with "Docker image: custom:tag"** + - Input: Shows "custom:tag" + - Runs with: custom:tag + +3. **User types "override:latest"** + - Input: Shows "override:latest" + - Runs with: override:latest + +4. **User clears input** + - Input: Empty + - Runs with: File image or default + +5. **URL: ?docker_image=url:tag** + - Input: Shows "url:tag" + - Runs with: url:tag + +## Troubleshooting + +### Input field shows image but I didn't enter it +- Check if URL has `?docker_image=...` parameter +- Check if file contains `Docker image: ...` metadata +- This is expected behavior - showing what will be used + +### Clearing input doesn't work +- Make sure to blur the input (click outside or press Tab) +- Check browser console for errors +- Verify `/api/config` endpoint is accessible + +### Default image not from environment +- Verify `DOCKER_IMAGE` is set in `.env` file +- Restart backend server after changing `.env` +- Check backend logs for config loading + +### URL parameter not working +- Ensure parameter name is `docker_image` (underscore, not dash) +- Check browser console for URL parsing +- Verify FileExplorer component is mounted + +## API Reference + +### GET /api/config + +Returns configuration including default docker image. + +**Authentication:** Required + +**Response:** +```json +{ + "dockerImage": "ghcr.io/manticoresoftware/manticoresearch:test-kit-latest" +} +``` + +**Usage:** +```javascript +const response = await fetch('/api/config', { credentials: 'include' }); +const config = await response.json(); +console.log(config.dockerImage); +``` + +## Migration Notes + +### From Hardcoded Default + +**Before:** +```typescript +dockerImage: 'ghcr.io/manticoresoftware/manticoresearch:test-kit-latest' +``` + +**After:** +```bash +# Set in .env +DOCKER_IMAGE=ghcr.io/manticoresoftware/manticoresearch:test-kit-latest +``` + +### From Always-Filled Input + +**Before:** Input always showed current docker image + +**After:** Input is empty unless explicitly set by user/URL/file + +**Benefit:** Clearer UX - empty = default, filled = override diff --git a/ui/README.md b/ui/README.md index efc192b..cc01566 100644 --- a/ui/README.md +++ b/ui/README.md @@ -127,6 +127,7 @@ You can configure the server to listen on different ports and hosts: HOST=localhost # Set to '0.0.0.0' to listen on all interfaces FRONTEND_PORT=5173 # Default frontend port (Vite) BACKEND_PORT=3000 # Default backend port (Express) +DOCKER_IMAGE=ghcr.io/manticoresoftware/manticoresearch:test-kit-latest # Default docker image for tests ``` ### Development Mode @@ -145,7 +146,7 @@ This will bypass the authentication check and allow you to access the applicatio 2. The file explorer on the left shows available .rec files 3. Click on a file to open it in the editor 4. Add commands and expected outputs -5. Configure the Docker image at the top for validation +5. Configure the Docker image at the top for validation (see [DOCKER_IMAGE.md](./DOCKER_IMAGE.md) for details) 6. Save files as needed 7. Run tests to see real-time diff comparison with pattern recognition @@ -158,6 +159,12 @@ This will bypass the authentication check and allow you to access the applicatio - `pkg/` - WebAssembly module for diff comparison (compiled from wasm-diff) - `.clt/patterns` - Pattern definitions for variable matching in tests +## Documentation + +- [API.md](./API.md) - Complete API endpoints reference +- [DOCKER_IMAGE.md](./DOCKER_IMAGE.md) - Docker image configuration guide +- [INSTALL.md](./INSTALL.md) - Installation instructions + ## Development ### Building the wasm-diff Module @@ -201,6 +208,7 @@ npm run test ```javascript GET /api/get-file-tree # Hierarchical file listing GET /api/get-file # File content retrieval + GET /api/config # Configuration (docker image, etc.) POST /api/save-file # File content saving POST /api/move-file # File/directory movement DELETE /api/delete-file # File/directory deletion diff --git a/ui/gitRoutes.js b/ui/gitRoutes.js index c3b1401..4e5396c 100644 --- a/ui/gitRoutes.js +++ b/ui/gitRoutes.js @@ -8,7 +8,7 @@ import { slugify } from './routes.js'; import { ensureRepositoryCheckout } from './repositoryManager.js'; -import { getDefaultBranch, checkExistingPR, isPRBranch } from './helpers.js'; +import { getDefaultBranch, checkExistingPR, isPRBranch, isOnDefaultBranch } from './helpers.js'; import tokenManager from './tokenManager.js'; // Setup Git routes @@ -625,6 +625,17 @@ export function setupGitRoutes(app, isAuthenticated, dependencies) { const username = req.user.username; const userRepo = getUserRepoPath(req, WORKDIR, ROOT_DIR, getAuthConfig); + // Check if on default branch - block commits + const branchCheck = await isOnDefaultBranch(userRepo); + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot commit changes on default branch (${branchCheck.defaultBranch}). Please create a new branch before committing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + const git = simpleGit({ baseDir: userRepo }); await ensureGitRemoteWithToken(git, req.user.token, REPO_URL); @@ -749,6 +760,19 @@ export function setupGitRoutes(app, isAuthenticated, dependencies) { }); } + // Pull latest changes from remote branch before modifying + console.log(`🔄 Pulling latest changes from origin/${currentBranch}...`); + try { + await git.pull('origin', currentBranch, ['--rebase']); + console.log('✅ Successfully pulled latest changes'); + } catch (pullError) { + console.error('❌ Pull failed:', pullError); + return res.status(409).json({ + error: 'Failed to pull latest changes from remote branch. There may be conflicts or the branch may have diverged.', + details: pullError.message + }); + } + // Remove [skip ci] from commit message const newCommitMessage = lastCommitMessage.replace(skipCiPattern, '').trim(); @@ -794,6 +818,17 @@ export function setupGitRoutes(app, isAuthenticated, dependencies) { return res.status(404).json({ error: 'Repository not found' }); } + // Check if on default branch - block file checkout (discarding changes) + const branchCheck = await isOnDefaultBranch(userRepo); + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot discard file changes on default branch (${branchCheck.defaultBranch}). Please create a new branch before editing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + try { // Initialize simple-git with the user's repo path const git = simpleGit(userRepo); @@ -1094,6 +1129,17 @@ export function setupGitRoutes(app, isAuthenticated, dependencies) { return res.status(404).json({ error: 'Repository not found' }); } + // Check if on default branch - block undo operations + const branchCheck = await isOnDefaultBranch(userRepo); + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot undo changes on default branch (${branchCheck.defaultBranch}). Please create a new branch before editing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + const git = simpleGit({ baseDir: userRepo }); const isRepo = await git.checkIsRepo(); @@ -1225,6 +1271,17 @@ export function setupGitRoutes(app, isAuthenticated, dependencies) { return res.status(404).json({ error: 'Repository not found' }); } + // Check if on default branch - block redo operations + const branchCheck = await isOnDefaultBranch(userRepo); + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot redo changes on default branch (${branchCheck.defaultBranch}). Please create a new branch before editing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + const git = simpleGit({ baseDir: userRepo }); const isRepo = await git.checkIsRepo(); diff --git a/ui/helpers.js b/ui/helpers.js index 8672c8a..75d94ee 100644 --- a/ui/helpers.js +++ b/ui/helpers.js @@ -376,6 +376,35 @@ export function updateSessionMetadata(session) { } } +/** + * Check if current branch is a default branch (main/master) + * Reuses logic from autoCommitAndPush to avoid duplication + * @param {string} userRepoPath - Path to user repository + * @returns {Promise} Object with isDefault boolean and branch names + */ +export async function isOnDefaultBranch(userRepoPath) { + try { + const git = simpleGit(userRepoPath); + + const isRepo = await git.checkIsRepo(); + if (!isRepo) { + return { isDefault: false, currentBranch: null, defaultBranch: null }; + } + + const currentBranch = await git.revparse(['--abbrev-ref', 'HEAD']); + const defaultBranch = await getDefaultBranch(git, userRepoPath); + + return { + isDefault: currentBranch === defaultBranch, + currentBranch, + defaultBranch + }; + } catch (error) { + console.error('Error checking default branch:', error); + return { isDefault: false, currentBranch: null, defaultBranch: null }; + } +} + /** * Auto-commit and push changes when not on default branch * @param {string} userRepoPath - Path to user repository diff --git a/ui/routes.js b/ui/routes.js index da3a8db..35d7810 100644 --- a/ui/routes.js +++ b/ui/routes.js @@ -7,7 +7,7 @@ import { generateRecFileToMapWasm, validateTestFromMapWasm } from './wasmNodeWrapper.js'; -import { autoCommitAndPush } from './helpers.js'; +import { autoCommitAndPush, isOnDefaultBranch } from './helpers.js'; // Helper functions that were in server.js export function getUserRepoPath(req, WORKDIR, ROOT_DIR, getAuthConfig) { @@ -384,6 +384,13 @@ export function setupRoutes(app, isAuthenticated, dependencies) { }); }); + // API endpoint to get configuration (including default docker image) + app.get('/api/config', isAuthenticated, (req, res) => { + return res.json({ + dockerImage: process.env.DOCKER_IMAGE || 'ghcr.io/manticoresoftware/manticoresearch:test-kit-latest' + }); + }); + // API endpoint to get the file tree app.get('/api/get-file-tree', isAuthenticated, async (req, res) => { try { @@ -492,6 +499,19 @@ export function setupRoutes(app, isAuthenticated, dependencies) { return res.status(400).json({ error: 'File path is required' }); } + // Check if on default branch - block file modifications + const userRepoPath = getUserRepoPath(req, WORKDIR, ROOT_DIR, getAuthConfig); + const branchCheck = await isOnDefaultBranch(userRepoPath); + + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot save files on default branch (${branchCheck.defaultBranch}). Please create a new branch before editing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + // Use the user's test directory as the base const testDir = getUserTestPath(req, WORKDIR, ROOT_DIR, getAuthConfig); const absolutePath = path.join(testDir, filePath); @@ -607,6 +627,19 @@ export function setupRoutes(app, isAuthenticated, dependencies) { return res.status(400).json({ error: 'Source and target paths are required' }); } + // Check if on default branch - block file modifications + const userRepoPath = getUserRepoPath(req, WORKDIR, ROOT_DIR, getAuthConfig); + const branchCheck = await isOnDefaultBranch(userRepoPath); + + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot move files on default branch (${branchCheck.defaultBranch}). Please create a new branch before editing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + // Use the user's test directory as the base const testDir = getUserTestPath(req, WORKDIR, ROOT_DIR, getAuthConfig); const absoluteSourcePath = path.join(testDir, sourcePath); @@ -640,6 +673,19 @@ export function setupRoutes(app, isAuthenticated, dependencies) { return res.status(400).json({ error: 'File path is required' }); } + // Check if on default branch - block file modifications + const userRepoPath = getUserRepoPath(req, WORKDIR, ROOT_DIR, getAuthConfig); + const branchCheck = await isOnDefaultBranch(userRepoPath); + + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot delete files on default branch (${branchCheck.defaultBranch}). Please create a new branch before editing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + // Use the user's test directory as the base const testDir = getUserTestPath(req, WORKDIR, ROOT_DIR, getAuthConfig); const absolutePath = path.join(testDir, filePath); @@ -664,7 +710,7 @@ export function setupRoutes(app, isAuthenticated, dependencies) { // Attempt auto-commit and push if not on default branch console.log('🚀 [DELETE-FILE] Attempting auto-commit and push...'); - const userRepoPath = getUserRepoPath(req, WORKDIR, ROOT_DIR, getAuthConfig); + // userRepoPath already declared above at line 670 console.log('🚀 [DELETE-FILE] User repo path:', userRepoPath); console.log('🚀 [DELETE-FILE] File path for commit:', filePath); @@ -694,6 +740,19 @@ export function setupRoutes(app, isAuthenticated, dependencies) { return res.status(400).json({ error: 'Directory path is required' }); } + // Check if on default branch - block directory creation + const userRepoPath = getUserRepoPath(req, WORKDIR, ROOT_DIR, getAuthConfig); + const branchCheck = await isOnDefaultBranch(userRepoPath); + + if (branchCheck.isDefault) { + return res.status(403).json({ + error: `Cannot create directories on default branch (${branchCheck.defaultBranch}). Please create a new branch before editing.`, + currentBranch: branchCheck.currentBranch, + defaultBranch: branchCheck.defaultBranch, + isDefaultBranch: true + }); + } + // Use the user's test directory as the base const testDir = getUserTestPath(req, WORKDIR, ROOT_DIR, getAuthConfig); const absolutePath = path.join(testDir, dirPath); diff --git a/ui/src/components/FileEditorModal.svelte b/ui/src/components/FileEditorModal.svelte index 0dcd0b4..bdf0b10 100644 --- a/ui/src/components/FileEditorModal.svelte +++ b/ui/src/components/FileEditorModal.svelte @@ -2,6 +2,7 @@ import { createEventDispatcher } from 'svelte'; import SimpleCodeMirror from './SimpleCodeMirror.svelte'; import { API_URL } from '../config.js'; + import { branchStore } from '../stores/branchStore'; export let visible = false; export let filePath: string | null = null; @@ -14,6 +15,13 @@ let error: string | null = null; let saving = false; let successMessage: string | null = null; + + // Check if on default branch (master/main) - READ ONLY mode + $: isOnDefaultBranch = $branchStore.currentBranch === $branchStore.defaultBranch; + $: isReadOnly = isOnDefaultBranch; + + // DEBUG LOG + $: console.log('🔒 FileEditorModal - currentBranch:', $branchStore.currentBranch, 'defaultBranch:', $branchStore.defaultBranch, 'isReadOnly:', isReadOnly); // Load file content when modal opens $: if (visible && filePath) { @@ -160,7 +168,7 @@