# Workspaces & Git

The **Workspace** is a virtual filesystem for the editor surface.
Each workspace has a root directory, file operations, optional Git
integration, and full-text indexing for code search.

**All endpoints require:** `Auth()` middleware (JWT bearer token).
Workspace-scoped endpoints additionally verify ownership via
`loadAndAuthorize` (user owns workspace, or is member of the
owning team/project/channel).

---

### Workspace CRUD

```
GET    /workspaces                  → { "data": [...] }
POST   /workspaces                  ← { "name", "owner_type", "owner_id" }
GET    /workspaces/:id              → workspace object
PATCH  /workspaces/:id              ← partial update
DELETE /workspaces/:id
```

**Auth:** Authenticated user. `GET /workspaces` returns user-owned
workspaces plus those owned by the user's teams. All `:id` endpoints
verify the caller can access the workspace's owner entity.

Workspace object:

```json
{
  "id": "uuid",
  "name": "my-project",
  "owner_type": "user",
  "owner_id": "uuid",
  "status": "active",
  "max_bytes": 52428800,
  "indexing_enabled": false,
  "git_remote_url": "https://github.com/...",
  "git_branch": "main",
  "git_credential_id": "uuid",
  "git_last_sync": "2025-06-15T14:30:00Z",
  "file_count": 42,
  "total_bytes": 1048576,
  "created_at": "2025-06-15T14:30:00Z",
  "updated_at": "2025-06-15T14:30:00Z"
}
```

| Field | Type | Notes |
|-------|------|-------|
| `id` | string | UUIDv4 |
| `name` | string | Max 200 chars |
| `owner_type` | `"user"` \| `"project"` \| `"channel"` \| `"team"` | Owner entity type |
| `owner_id` | string | UUID of owner entity |
| `status` | `"active"` \| `"archived"` \| `"deleting"` | Lifecycle state |
| `max_bytes` | int \| null | Quota ceiling. Omitted if unset. |
| `indexing_enabled` | bool | Full-text search indexing |
| `git_remote_url` | string \| null | Git remote. Omitted if unset. |
| `git_branch` | string \| null | Tracked branch. Omitted if unset. |
| `git_credential_id` | string \| null | FK to git credential. Omitted if unset. |
| `git_last_sync` | string \| null | ISO 8601. Omitted if never synced. |
| `file_count` | int | Computed. Enriched on `GET /:id` only. |
| `total_bytes` | int | Computed. Enriched on `GET /:id` only. |
| `created_at` | string | ISO 8601 |
| `updated_at` | string | ISO 8601 |

> **Note:** `root_path` is a server-internal filesystem path and is
> never exposed in API responses (json tag `"-"`).

**Create request:**

```json
{
  "name": "my-project",
  "owner_type": "user",
  "owner_id": "uuid",
  "max_bytes": 52428800
}
```

Returns `201` with the workspace object.

**PATCH request** (all fields optional):

```json
{
  "name": "new-name",
  "status": "archived",
  "max_bytes": 104857600,
  "indexing_enabled": true,
  "git_remote_url": "https://...",
  "git_branch": "develop",
  "git_credential_id": "uuid"
}
```

Returns updated workspace object.

**DELETE** returns `{"ok": true}`. Deletion is async (status set to
`"deleting"`, filesystem cleanup runs in background).

---

### File Operations

All paths are relative to the workspace root. File path is passed
as `?path=` query parameter.

```
GET    /workspaces/:id/files?path=/           → { "data": [entries] }
GET    /workspaces/:id/files/read?path=/a.md  → raw file content (binary stream)
PUT    /workspaces/:id/files/write?path=/a.md ← raw body (Content-Length required)
DELETE /workspaces/:id/files/delete?path=/a.md
POST   /workspaces/:id/files/mkdir?path=/src
```

**Auth:** Authenticated user with workspace access.

**`GET .../files`** accepts `?recursive=true` for deep listing.
Returns `{"data": [...]}` (never bare array). Nil slice guarded.

File entry (from `WorkspaceFile` model):

```json
{
  "id": "uuid",
  "workspace_id": "uuid",
  "path": "/src/main.go",
  "is_directory": false,
  "content_type": "text/x-go",
  "size_bytes": 4096,
  "sha256": "abc123...",
  "index_status": "ready",
  "chunk_count": 3,
  "created_at": "2025-06-15T14:30:00Z",
  "updated_at": "2025-06-15T14:30:00Z"
}
```

**`GET .../files/read`** returns raw file content with appropriate
`Content-Type` header and `Content-Length`. This is a binary stream,
not JSON.

**`PUT .../files/write`** reads the request body as raw content.
Returns `{"ok": true, "path": "/a.md"}`. Returns `413` if the
write would exceed the workspace quota.

**`DELETE .../files/delete`** accepts `?recursive=true` for
directory removal. Returns `{"ok": true}`.

**`POST .../files/mkdir`** returns `201 {"ok": true, "path": "/src"}`.

---

### Archive

**Download** (full workspace as archive):

```
GET /workspaces/:id/archive/download?format=zip
```

**Auth:** Authenticated user with workspace access.

`format` query param: `zip` (default), `tar.gz`, `tgz`.
Returns binary stream with `Content-Disposition: attachment`.

**Upload** (restore from archive):

```
POST /workspaces/:id/archive/upload
Content-Type: multipart/form-data (field: "file")
```

**Auth:** Authenticated user with workspace access.

Accepts `.zip`, `.tar.gz`, `.tgz` archives. Format detected from
filename extension.

Returns:

```json
{
  "ok": true,
  "files_extracted": 42
}
```

---

### Reconcile, Stats, Index Status

**Reconcile** (sync filesystem state with DB metadata):

```
POST /workspaces/:id/reconcile
```

**Auth:** Authenticated user with workspace access.

Returns:

```json
{
  "added": 5,
  "removed": 2,
  "updated": 3
}
```

**Stats:**

```
GET /workspaces/:id/stats
```

**Auth:** Authenticated user with workspace access.

Returns a `WorkspaceStats` object:

```json
{
  "workspace_id": "uuid",
  "file_count": 42,
  "dir_count": 8,
  "total_bytes": 1048576,
  "max_bytes": 52428800
}
```

**Index status:**

```
GET /workspaces/:id/index-status
```

**Auth:** Authenticated user with workspace access.

Returns indexing progress for full-text search:

```json
{
  "workspace_id": "uuid",
  "indexing_enabled": true,
  "status_counts": {
    "pending": 3,
    "indexing": 1,
    "ready": 38,
    "error": 0,
    "skipped": 2
  },
  "total_chunks": 156
}
```

---

### Git Integration

All Git operations are workspace-scoped. Require the workspace to
have a Git remote configured.

```
POST /workspaces/:id/git/clone     ← { "url", "branch", "credential_id" }
POST /workspaces/:id/git/pull
POST /workspaces/:id/git/push
GET  /workspaces/:id/git/status    → GitStatus object
GET  /workspaces/:id/git/diff      → { "diff": "..." }
POST /workspaces/:id/git/commit    ← { "message", "paths": [...] }
GET  /workspaces/:id/git/log       → { "data": [GitLogEntry] }
GET  /workspaces/:id/git/branches  → { "branches": [...], "current": "main" }
POST /workspaces/:id/git/checkout  ← { "branch": "feature-x" }
```

**Auth:** Authenticated user with workspace access.

**`POST .../git/clone`** request:

```json
{
  "url": "https://github.com/user/repo.git",
  "branch": "main",
  "credential_id": "uuid"
}
```

`branch` and `credential_id` are optional. Returns `{"status": "cloned"}`.

**`POST .../git/pull`** returns `{"status": "pulled"}`.

**`POST .../git/push`** returns `{"status": "pushed"}`.

**`GET .../git/status`** returns the full `GitStatus` object:

```json
{
  "branch": "main",
  "clean": false,
  "ahead": 2,
  "behind": 0,
  "staged": [{ "path": "file.go", "status": "M" }],
  "modified": [{ "path": "other.go", "status": "M" }],
  "untracked": ["new-file.txt"]
}
```

**`GET .../git/diff`** accepts optional `?path=file.go` for single-file diff.
Returns `{"diff": "..."}` (unified diff string).

**`POST .../git/commit`** request:

```json
{
  "message": "fix: resolve null pointer",
  "paths": ["/src/main.go", "/src/util.go"]
}
```

`paths` is optional (commits all staged if omitted). Returns
`{"status": "committed"}`.

**`GET .../git/log`** accepts optional `?n=20` (default 20, max 100).
Returns `{"data": [GitLogEntry]}`:

```json
{
  "data": [
    {
      "hash": "abc123...",
      "short_hash": "abc123",
      "author": "Jane Doe",
      "date": "2025-06-15T14:30:00Z",
      "message": "Initial commit"
    }
  ]
}
```

**`GET .../git/branches`** returns:

```json
{
  "current": "main",
  "branches": ["main", "develop", "feature-x"]
}
```

**`POST .../git/checkout`** returns
`{"status": "checked out", "branch": "feature-x"}`.

---

### Git Credentials

User-owned, encrypted credential storage for Git operations.
Encrypted with the user's UEK via the platform vault (same pattern
as BYOK keys — admins cannot recover).

Independent of workspace scope — credentials are user-level.

```
POST   /git-credentials             ← { "name", "auth_type", ... }
POST   /git-credentials/generate    ← { "name", "persona_id?" }
GET    /git-credentials             → { "data": [GitCredentialSummary] }
GET    /git-credentials/:id/public-key → { "public_key", "fingerprint" }
DELETE /git-credentials/:id
```

**Auth:** Authenticated user. Credentials are scoped to the requesting user.

**Create request (manual):**

`auth_type` determines which fields are required:

| `auth_type` | Required fields |
|-------------|----------------|
| `https_pat` | `token` |
| `https_basic` | `username`, `password` |
| `ssh_key` | `private_key` (+ optional `passphrase`) |

Returns `201` with the credential summary (never exposes encrypted data).

**Generate request (v0.28.6 — server-side SSH keygen):**

```json
{
  "name": "GitHub - main",
  "persona_id": "uuid"
}
```

Generates an ED25519 SSH keypair server-side. The private key is
vault-encrypted before storage and never leaves the server. Returns
the credential summary including the public key (authorized_keys
format) and SHA256 fingerprint. The user adds the public key to their
git host (GitHub, Gitea, etc.).

Optional `persona_id` binds the key to a persona for commit
attribution — git operations using this credential will set the
commit author to the persona's name and handle.

Returns `201`.

**Get Public Key:**

```
GET /git-credentials/:id/public-key
```

Returns `{ "public_key": "ssh-ed25519 AAAA...", "fingerprint": "SHA256:..." }`.
Owner-only — returns `404` for other users or credentials without
a public key (e.g. PAT credentials).

**List response:**

```json
{
  "data": [
    {
      "id": "uuid",
      "name": "GitHub PAT",
      "auth_type": "https_pat",
      "public_key": "",
      "fingerprint": "",
      "persona_id": null,
      "created_at": "2025-06-15T14:30:00Z"
    }
  ]
}
```

`public_key` and `fingerprint` are populated for `ssh_key` type
credentials created via `/generate`. Empty for PAT/basic auth.

**DELETE** returns `{"deleted": true}`. Returns `404` if credential
not found or not owned by the requesting user.

---
