feat: config profile multi-repo mounts
- Add mappings array support to git_mount entries - Clone repository once per git_mount entry, mount multiple subdirectories - Normalize legacy source_path+target_path to mappings on read - Update _merge_git_mounts to dedup by (remote_url, branch) and concatenate mappings - Add _normalize_git_mount, _clone_git_repo, _resolve_git_mount_mappings helpers - Update GitMountItem Pydantic model with GitMountMapping and model_validator - Update frontend GitMountEditor component with mappings UI - Auto-convert legacy git mount entries to mappings format on load - Add 15 backend unit tests for normalization, resolution, and glob expansion - Update existing config profile resolver tests for new merge behavior Quality gates: pytest 167 passed, frontend typecheck clean Addresses: config-profile-multi-repo-mounts
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
name: config-profile-multi-repo-mounts
|
||||
status: completed
|
||||
phase: verify
|
||||
parent: null
|
||||
type: feature
|
||||
description: Enable multiple source/target mappings per git mount entry in Config Profiles, cloning the repository only once per entry.
|
||||
created_at: 2026-05-28
|
||||
updated_at: 2026-05-28
|
||||
@@ -0,0 +1,172 @@
|
||||
# Design: Config Profile Multi-Repo Mounts
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data Model
|
||||
|
||||
No database changes. The `git_mounts` JSONB column already stores arbitrary JSON.
|
||||
|
||||
#### Normalized git mount schema (in memory)
|
||||
|
||||
After validation/normalization, every git mount entry is converted to the unified form:
|
||||
|
||||
```python
|
||||
{
|
||||
"remote_url": str,
|
||||
"branch": str | None,
|
||||
"mappings": [
|
||||
{"source_path": str, "target_path": str},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The normalization step converts legacy `source_path` + `target_path` into a single-entry `mappings` array.
|
||||
|
||||
### Backend Changes
|
||||
|
||||
#### 1. `_resolve_single_git_mount` refactor
|
||||
|
||||
Split into two functions:
|
||||
|
||||
**`_clone_git_repo(remote_url, branch, clone_parent) -> repo_path`**
|
||||
- Clones or pulls the repo
|
||||
- Returns the path to `repo-clone`
|
||||
- Same as before, but extracts the clone logic
|
||||
|
||||
**`_resolve_git_mount_mappings(repo_path, mappings, working_directory) -> list[dict]`**
|
||||
- Takes the already-cloned repo path
|
||||
- For each mapping:
|
||||
1. Build source path: `os.path.join(repo_path, mapping["source_path"])`
|
||||
2. Expand globs via `_expand_glob_source`
|
||||
3. Resolve target path (absolute or relative to working_directory)
|
||||
4. Build volume mount entries
|
||||
- Returns list of volume mount dicts
|
||||
|
||||
**`_resolve_single_git_mount` new flow:**
|
||||
1. Validate entry (remote_url, mappings or source_path+target_path)
|
||||
2. Normalize legacy format to `mappings` array
|
||||
3. Compute clone directory (same as before: `git-mounts/{repo_name}-{hash}/`)
|
||||
4. Clone/pull repo
|
||||
5. Resolve all mappings from the cloned repo
|
||||
6. Return flat list of volume mounts
|
||||
|
||||
#### 2. `_merge_git_mounts` update
|
||||
|
||||
The merge key changes from `(remote_url, target_path)` to `(remote_url, branch)`.
|
||||
|
||||
When two entries have the same `remote_url` and `branch`, their `mappings` arrays are concatenated. When different, they are kept as separate entries.
|
||||
|
||||
```python
|
||||
def _merge_git_mounts(base, overlay, source_name):
|
||||
result = list(base)
|
||||
seen = {}
|
||||
for i, m in enumerate(result):
|
||||
key = (m["remote_url"], m.get("branch"))
|
||||
seen[key] = i
|
||||
|
||||
for mount in overlay:
|
||||
key = (mount["remote_url"], mount.get("branch"))
|
||||
if key in seen:
|
||||
# Same repo+branch: concatenate mappings
|
||||
result[seen[key]]["mappings"].extend(mount.get("mappings", []))
|
||||
else:
|
||||
seen[key] = len(result)
|
||||
result.append(dict(mount))
|
||||
return result
|
||||
```
|
||||
|
||||
#### 3. Validation on save
|
||||
|
||||
In the Config Profile API (create/update), validate `git_mounts`:
|
||||
- Each entry must have `remote_url`
|
||||
- Each entry must have either `mappings` OR (`source_path` AND `target_path`)
|
||||
- Each mapping must have `source_path` and `target_path`
|
||||
- `mappings` must be a non-empty array
|
||||
|
||||
### Frontend Changes
|
||||
|
||||
#### GitMountEditor component
|
||||
|
||||
New or updated component for editing a single git mount entry:
|
||||
|
||||
```
|
||||
Remote URL: [____________________]
|
||||
Branch: [main________________]
|
||||
|
||||
Mappings:
|
||||
Source Path → Target Path
|
||||
[packages/api ] [/app/api ] [×]
|
||||
[packages/web ] [/app/web ] [×]
|
||||
[ ] [ ] [+ Add]
|
||||
```
|
||||
|
||||
**State shape:**
|
||||
```typescript
|
||||
interface GitMountMapping {
|
||||
source_path: string;
|
||||
target_path: string;
|
||||
}
|
||||
|
||||
interface GitMountEntry {
|
||||
remote_url: string;
|
||||
branch?: string;
|
||||
mappings: GitMountMapping[];
|
||||
// Legacy fields (read-only for old data)
|
||||
source_path?: string;
|
||||
target_path?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Migration on load:** If an entry has `source_path` and `target_path` but no `mappings`, auto-convert:
|
||||
```typescript
|
||||
if (!entry.mappings && entry.source_path && entry.target_path) {
|
||||
entry.mappings = [{ source_path: entry.source_path, target_path: entry.target_path }];
|
||||
}
|
||||
```
|
||||
|
||||
### File Changes
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `apps/api/src/api/tool_instances.py` | Refactor `_resolve_single_git_mount` to support mappings |
|
||||
| `apps/api/src/services/config_profile_resolver.py` | Update `_merge_git_mounts` merge key |
|
||||
| `apps/api/src/api/config_profiles.py` | Add validation for git_mounts schema |
|
||||
| `apps/web/src/components/config-profile-editor.tsx` | Add mappings UI for git mounts |
|
||||
| `apps/web/src/api/config_profiles.ts` | Update types for GitMountEntry |
|
||||
| `apps/api/tests/unit/test_git_mounts.py` | New unit tests for multi-mapping resolution |
|
||||
| `apps/api/tests/unit/test_config_profile_resolver.py` | Update merge tests |
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
#### Backend unit tests
|
||||
|
||||
1. `_resolve_single_git_mount` with 3 mappings → single clone, 3 mounts
|
||||
2. `_resolve_single_git_mount` legacy format → single clone, 1 mount
|
||||
3. `_merge_git_mounts` same repo+branch → mappings concatenated
|
||||
4. `_merge_git_mounts` different repos → separate entries
|
||||
5. Validation: entry with neither mappings nor source_path → error
|
||||
6. Validation: mapping missing target_path → error
|
||||
|
||||
#### Integration tests
|
||||
|
||||
1. Create profile with 2 mappings from same repo → start instance → verify single clone directory
|
||||
2. Create profile with legacy format → start instance → verify backward compatibility
|
||||
|
||||
#### Frontend tests
|
||||
|
||||
1. GitMountEditor renders mappings list
|
||||
2. Adding a mapping updates state correctly
|
||||
3. Legacy entry auto-converts on load
|
||||
4. Save sends correct JSON shape
|
||||
|
||||
### Migration Plan
|
||||
|
||||
No database migration. Existing `git_mounts` JSON continues to work because:
|
||||
- The code normalizes legacy `source_path` + `target_path` to `mappings` on read
|
||||
- The frontend auto-converts on load
|
||||
- New saves use the `mappings` format
|
||||
|
||||
### Rollback Plan
|
||||
|
||||
Since there is no schema change, rollback is just reverting the code. Existing profiles with the new `mappings` format will still parse correctly even with old code if we keep the normalization shim.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Exploration: Config Profile Multi-Repo Mounts
|
||||
|
||||
## Problem
|
||||
|
||||
Config Profiles support `git_mounts` — cloning repositories and mounting them into containers. However, each `git_mount` entry clones **one** source path from **one** repo. If a user wants to mount multiple directories from the same repository (e.g., a monorepo), they must add multiple `git_mount` entries, which results in **cloning the same repository multiple times**.
|
||||
|
||||
### Current git_mount schema (one mapping per entry)
|
||||
|
||||
```json
|
||||
{
|
||||
"remote_url": "https://github.com/user/monorepo.git",
|
||||
"source_path": "packages/backend",
|
||||
"target_path": "/app/backend",
|
||||
"branch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
To mount 3 directories from the same monorepo, the profile needs 3 entries, each triggering a separate clone of the full repository.
|
||||
|
||||
## Pain Points
|
||||
|
||||
1. **Redundant clones**: Cloning the same repo N times wastes time and disk space.
|
||||
2. **Slow instance startup**: Each clone adds 5-30 seconds depending on repo size.
|
||||
3. **Inconsistent branch state**: Each entry independently checks out the branch — they could drift if the branch moves between clones.
|
||||
4. **Poor monorepo support**: Monorepos are common; users expect to mount multiple packages.
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Scenario A: Monorepo with multiple packages
|
||||
|
||||
User has a monorepo `corp/monorepo` with:
|
||||
- `packages/api` → needs to be at `/app/api`
|
||||
- `packages/web` → needs to be at `/app/web`
|
||||
- `packages/shared` → needs to be at `/app/shared`
|
||||
|
||||
They want to mount all three into a single tool instance.
|
||||
|
||||
### Scenario B: Docs + Code sidecar
|
||||
|
||||
User wants to mount both:
|
||||
- `src/` → `/workspace/src`
|
||||
- `docs/` → `/workspace/docs`
|
||||
|
||||
from the same repo.
|
||||
|
||||
### Scenario C: Backward compatibility
|
||||
|
||||
Existing profiles with single `source_path`/`target_path` should continue working without migration.
|
||||
|
||||
## Design Directions
|
||||
|
||||
### Direction A: `mappings` array on git_mount entry
|
||||
|
||||
Add a `mappings` array to each git_mount entry. The repo is cloned once, and each mapping creates a separate bind mount.
|
||||
|
||||
```json
|
||||
{
|
||||
"remote_url": "https://github.com/user/monorepo.git",
|
||||
"branch": "main",
|
||||
"mappings": [
|
||||
{"source_path": "packages/api", "target_path": "/app/api"},
|
||||
{"source_path": "packages/web", "target_path": "/app/web"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- Clean, explicit grouping
|
||||
- Single clone per `remote_url + branch` combo
|
||||
- Easy to understand
|
||||
- Backward-compatible: legacy `source_path` + `target_path` can be treated as a single-entry `mappings` array
|
||||
|
||||
**Cons**:
|
||||
- Slightly more verbose JSON
|
||||
- Frontend form needs a nested list UI
|
||||
|
||||
### Direction B: Auto-dedup by remote_url + branch
|
||||
|
||||
Keep the flat list format, but internally group entries by `remote_url + branch` and clone once.
|
||||
|
||||
```json
|
||||
[
|
||||
{"remote_url": "...", "source_path": "a", "target_path": "/a", "branch": "main"},
|
||||
{"remote_url": "...", "source_path": "b", "target_path": "/b", "branch": "main"}
|
||||
]
|
||||
```
|
||||
|
||||
**Pros**:
|
||||
- No schema change
|
||||
- Transparent to users
|
||||
|
||||
**Cons**:
|
||||
- Magic behavior (not obvious why clones are shared)
|
||||
- Harder to reason about branch conflicts (what if same repo, different branches?)
|
||||
- Frontend doesn't show the grouping
|
||||
|
||||
### Direction C: Repo references + mount definitions split
|
||||
|
||||
Split into two concepts:
|
||||
1. `git_repos` — list of repos to clone (with branch)
|
||||
2. `git_mounts` — reference a repo by name and specify source/target
|
||||
|
||||
**Pros**:
|
||||
- Very explicit
|
||||
- Supports advanced scenarios (SSH keys per repo)
|
||||
|
||||
**Cons**:
|
||||
- Breaking schema change
|
||||
- Overkill for the current use case
|
||||
- Heavy migration burden
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Direction A (mappings array)** with backward-compatibility shim:
|
||||
- Add optional `mappings` field to git_mount entries
|
||||
- If `mappings` is absent, treat `source_path` + `target_path` as a single mapping
|
||||
- Clone once per `remote_url + branch`, apply all mappings from the same entry
|
||||
- No migration needed for existing data
|
||||
|
||||
This balances clarity, functionality, and backward compatibility.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Proposal: Config Profile Multi-Repo Mounts
|
||||
|
||||
## Context
|
||||
|
||||
Config Profiles allow users to mount external Git repositories into tool instances via `git_mounts`. Currently, each `git_mount` entry supports only **one** source_path → target_path mapping. If a user wants to mount multiple directories from the same repository (e.g., a monorepo), they must add multiple entries, each cloning the repository independently.
|
||||
|
||||
## Goal
|
||||
|
||||
Enable a single `git_mount` entry to declare **multiple** source/target mappings from the same repository, while cloning the repository only once per entry.
|
||||
|
||||
## Direction
|
||||
|
||||
**Direction A: `mappings` array with backward compatibility**
|
||||
|
||||
Add an optional `mappings` array to each `git_mount` entry. The repository is cloned once, and each mapping creates a separate bind mount from a subdirectory of the cloned repo.
|
||||
|
||||
If `mappings` is absent, the existing `source_path` + `target_path` fields are treated as a single mapping (backward-compatible).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. A `git_mount` entry can specify `mappings: [{"source_path": "...", "target_path": "..."}, ...]`
|
||||
2. The repository is cloned **exactly once** per `git_mount` entry
|
||||
3. Each mapping creates a separate Docker bind mount from the cloned repo subdirectory
|
||||
4. Existing profiles with `source_path`/`target_path` continue working without migration
|
||||
5. The Config Profile editor UI supports adding/removing mappings per git mount
|
||||
6. Glob patterns are supported in `source_path` within mappings
|
||||
7. Relative `target_path` values are resolved against `working_directory` as before
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Cross-entry repo deduplication (two separate git_mount entries with the same `remote_url` still clone twice)
|
||||
- SSH key per-repo configuration (can be added later)
|
||||
- Sparse checkout / partial clone optimization
|
||||
- Mounting from non-Git sources
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| Schema migration complexity | No migration needed — backward-compatible |
|
||||
| Frontend UI complexity | Nested form with add/remove mapping buttons |
|
||||
| Clone directory sharing race condition | Each entry gets its own clone directory (url_hash based) |
|
||||
| Large monorepo clone time | Out of scope — full clone is existing behavior |
|
||||
|
||||
## Related Artifacts
|
||||
|
||||
- Exploration: `openspec/explorations/config-profile-multi-repo-mounts.md`
|
||||
- Spec: `openspec/specs/config-profile-multi-repo-mounts.md`
|
||||
- Design: `openspec/designs/config-profile-multi-repo-mounts.md`
|
||||
- Tasks: `openspec/tasks/config-profile-multi-repo-mounts.md`
|
||||
@@ -0,0 +1,119 @@
|
||||
# Spec: Config Profile Multi-Repo Mounts
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
1. **FR-1**: A `git_mount` entry MAY include a `mappings` array.
|
||||
2. **FR-2**: Each item in `mappings` MUST have `source_path` and `target_path`.
|
||||
3. **FR-3**: If `mappings` is absent, `source_path` and `target_path` at the entry level MUST be treated as a single mapping (backward compatibility).
|
||||
4. **FR-4**: The repository MUST be cloned exactly once per `git_mount` entry.
|
||||
5. **FR-5**: Each mapping MUST create a separate Docker bind mount.
|
||||
6. **FR-6**: Glob patterns in `source_path` MUST be expanded per mapping.
|
||||
7. **FR-7**: Relative `target_path` values MUST be resolved against `working_directory`.
|
||||
8. **FR-8**: The merge logic for included profiles MUST deduplicate by `(remote_url, branch)` within a single resolved profile's `git_mounts` list.
|
||||
|
||||
### Non-Functional
|
||||
|
||||
1. **NFR-1**: No database schema migration required.
|
||||
2. **NFR-2**: Existing API responses must remain backward-compatible.
|
||||
3. **NFR-3**: Frontend type-check must pass without errors.
|
||||
|
||||
## API Contracts
|
||||
|
||||
### ConfigProfile model (git_mounts field)
|
||||
|
||||
```json
|
||||
{
|
||||
"git_mounts": [
|
||||
{
|
||||
"remote_url": "https://github.com/user/monorepo.git",
|
||||
"branch": "main",
|
||||
"mappings": [
|
||||
{"source_path": "packages/api", "target_path": "/app/api"},
|
||||
{"source_path": "packages/web", "target_path": "/app/web"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"remote_url": "https://github.com/user/docs.git",
|
||||
"source_path": ".",
|
||||
"target_path": "/docs",
|
||||
"branch": "main"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Validation rules
|
||||
|
||||
1. `mappings` must be a non-empty array if present.
|
||||
2. Each mapping must have `source_path` (string) and `target_path` (string).
|
||||
3. Either `mappings` OR (`source_path` AND `target_path`) must be present.
|
||||
4. `remote_url` must be a valid HTTPS or SSH Git URL.
|
||||
|
||||
## Database Schema
|
||||
|
||||
No changes. `git_mounts` is stored as JSONB in `config_profiles.git_mounts`.
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Scenario 1: Monorepo with multiple packages
|
||||
|
||||
**Given** a Config Profile with:
|
||||
```json
|
||||
{"git_mounts": [{
|
||||
"remote_url": "https://github.com/corp/monorepo.git",
|
||||
"branch": "main",
|
||||
"mappings": [
|
||||
{"source_path": "packages/api", "target_path": "/app/api"},
|
||||
{"source_path": "packages/web", "target_path": "/app/web"},
|
||||
{"source_path": "packages/shared", "target_path": "/app/shared"}
|
||||
]
|
||||
}]}
|
||||
```
|
||||
|
||||
**When** the profile is applied to an instance
|
||||
|
||||
**Then**:
|
||||
1. `corp/monorepo` is cloned once to `git-mounts/monorepo-{hash}/repo-clone`
|
||||
2. Three bind mounts are created:
|
||||
- `{clone}/packages/api` → `/app/api`
|
||||
- `{clone}/packages/web` → `/app/web`
|
||||
- `{clone}/packages/shared` → `/app/shared`
|
||||
|
||||
### Scenario 2: Legacy single mapping (backward compatibility)
|
||||
|
||||
**Given** a Config Profile with:
|
||||
```json
|
||||
{"git_mounts": [{
|
||||
"remote_url": "https://github.com/user/repo.git",
|
||||
"source_path": "src",
|
||||
"target_path": "/workspace/src",
|
||||
"branch": "main"
|
||||
}]}
|
||||
```
|
||||
|
||||
**When** the profile is applied
|
||||
|
||||
**Then** the behavior is identical to before (single clone, single mount).
|
||||
|
||||
### Scenario 3: Glob expansion within mapping
|
||||
|
||||
**Given** a mapping with:
|
||||
```json
|
||||
{"source_path": "packages/*", "target_path": "/app/packages"}
|
||||
```
|
||||
|
||||
**When** the repo is cloned and the glob is expanded
|
||||
|
||||
**Then** each matched directory is mounted as a separate bind mount with the relative path appended to the target:
|
||||
- `{clone}/packages/api` → `/app/packages/api`
|
||||
- `{clone}/packages/web` → `/app/packages/web`
|
||||
|
||||
## Test Strategy
|
||||
|
||||
1. Unit test `_resolve_single_git_mount` with `mappings` array
|
||||
2. Unit test `_merge_git_mounts` with mappings deduplication
|
||||
3. Integration test: profile with 3 mappings from same repo → verify single clone
|
||||
4. Integration test: legacy profile without `mappings` → verify backward compatibility
|
||||
5. Frontend unit test: GitMountEditor renders mappings form correctly
|
||||
@@ -0,0 +1,132 @@
|
||||
# Tasks: Config Profile Multi-Repo Mounts
|
||||
|
||||
## T1: Backend — Refactor git mount resolution for mappings
|
||||
|
||||
### T1.1: Normalize legacy git mount format
|
||||
**File**: `apps/api/src/api/tool_instances.py`
|
||||
- Add `_normalize_git_mount(entry: dict) -> dict` helper
|
||||
- Converts `{"source_path": "...", "target_path": "..."}` to `{"mappings": [{...}]}`
|
||||
- Call normalization at the start of `_resolve_single_git_mount`
|
||||
|
||||
### T1.2: Extract clone logic
|
||||
**File**: `apps/api/src/api/tool_instances.py`
|
||||
- Create `_clone_git_repo(remote_url, branch, clone_parent) -> str` function
|
||||
- Move clone/pull logic from `_resolve_single_git_mount` into it
|
||||
- Returns `repo_path` (path to `repo-clone`)
|
||||
|
||||
### T1.3: Resolve mappings from cloned repo
|
||||
**File**: `apps/api/src/api/tool_instances.py`
|
||||
- Create `_resolve_git_mount_mappings(repo_path, mappings, working_directory) -> list[dict]`
|
||||
- Iterates over mappings, expands globs, resolves targets
|
||||
- Returns flat list of volume mount dicts
|
||||
|
||||
### T1.4: Wire it together
|
||||
**File**: `apps/api/src/api/tool_instances.py`
|
||||
- Update `_resolve_single_git_mount` to:
|
||||
1. Normalize entry
|
||||
2. Clone repo once
|
||||
3. Resolve all mappings
|
||||
4. Return flat volume mounts
|
||||
|
||||
### T1.5: Update merge logic
|
||||
**File**: `apps/api/src/services/config_profile_resolver.py`
|
||||
- Change `_merge_git_mounts` merge key from `(remote_url, target_path)` to `(remote_url, branch)`
|
||||
- When same repo+branch: concatenate `mappings` arrays
|
||||
- When different: append as separate entry
|
||||
|
||||
### T1.6: Add validation
|
||||
**File**: `apps/api/src/api/config_profiles.py`
|
||||
- Validate `git_mounts` on create/update:
|
||||
- `remote_url` required
|
||||
- Either `mappings` (non-empty array) OR (`source_path` + `target_path`)
|
||||
- Each mapping has `source_path` and `target_path`
|
||||
|
||||
### T1.7: Unit tests
|
||||
**File**: `apps/api/tests/unit/test_git_mounts.py` (new)
|
||||
- Test normalization: legacy → mappings
|
||||
- Test single clone with 3 mappings → 3 volume mounts
|
||||
- Test glob expansion within mapping
|
||||
- Test relative target resolution
|
||||
|
||||
**File**: `apps/api/tests/unit/test_config_profile_resolver.py`
|
||||
- Update merge tests for new dedup key
|
||||
|
||||
---
|
||||
|
||||
## T2: Frontend — Git mount mappings editor
|
||||
|
||||
### T2.1: Update types
|
||||
**File**: `apps/web/src/api/config_profiles.ts`
|
||||
- Add `GitMountMapping` interface
|
||||
- Update `GitMountEntry` to have `mappings: GitMountMapping[]`
|
||||
- Keep optional `source_path`/`target_path` for backward compat
|
||||
|
||||
### T2.2: Auto-convert legacy entries on load
|
||||
**File**: `apps/web/src/components/config-profile-editor.tsx` or new `GitMountEditor.tsx`
|
||||
- On loading a profile, normalize any git_mount entries that lack `mappings`
|
||||
|
||||
### T2.3: Build mappings UI
|
||||
**File**: `apps/web/src/components/GitMountEditor.tsx` (new)
|
||||
- Render table/list of mappings per git mount entry
|
||||
- "Add mapping" button appends empty row
|
||||
- "Remove" button deletes a mapping row
|
||||
- Source path and target path inputs
|
||||
|
||||
### T2.4: Integrate into ConfigProfileEditor
|
||||
**File**: `apps/web/src/components/config-profile-editor.tsx`
|
||||
- Replace existing git_mounts flat form with GitMountEditor component
|
||||
- Ensure save sends correct JSON shape
|
||||
|
||||
### T2.5: Frontend tests
|
||||
**File**: `apps/web/src/components/GitMountEditor.test.tsx` (new)
|
||||
- Render with 2 mappings
|
||||
- Add mapping increases count
|
||||
- Remove mapping decreases count
|
||||
- Legacy entry auto-converts
|
||||
|
||||
---
|
||||
|
||||
## T3: Integration & Verification
|
||||
|
||||
### T3.1: Integration test
|
||||
**File**: `apps/api/tests/integration/test_config_profiles_git_mounts.py` (new)
|
||||
- Create profile with 2 mappings from same repo
|
||||
- Start instance
|
||||
- Verify single clone directory exists
|
||||
- Verify 2 bind mounts in compose file
|
||||
|
||||
### T3.2: Manual verification
|
||||
- Create a Config Profile with a monorepo git mount + 3 mappings
|
||||
- Create and start a pi-agent instance with the profile
|
||||
- Verify all 3 directories are mounted correctly
|
||||
- Verify legacy profile (single mapping) still works
|
||||
|
||||
### T3.3: Typecheck & tests
|
||||
```bash
|
||||
cd apps/web && npm run typecheck
|
||||
cd apps/api && pytest tests/unit/test_git_mounts.py tests/unit/test_config_profile_resolver.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Estimation
|
||||
|
||||
| Task | Effort | Files |
|
||||
|------|--------|-------|
|
||||
| T1.1-T1.4 | 2h | 1 |
|
||||
| T1.5 | 1h | 1 |
|
||||
| T1.6 | 1h | 1 |
|
||||
| T1.7 | 2h | 2 |
|
||||
| T2.1-T2.4 | 3h | 3 |
|
||||
| T2.5 | 1h | 1 |
|
||||
| T3.1-T3.3 | 2h | 2 |
|
||||
| **Total** | **12h** | **11** |
|
||||
|
||||
## PR Strategy
|
||||
|
||||
**Single PR** (~400 lines estimated, within review budget):
|
||||
- Backend changes (T1)
|
||||
- Frontend changes (T2)
|
||||
- Tests (T1.7, T2.5, T3.1)
|
||||
|
||||
All changes are tightly coupled (backend schema + frontend UI + tests) so a single PR is appropriate.
|
||||
Reference in New Issue
Block a user