Merge branch 'fusion/fn-002'
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.py]
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# App identity
|
||||||
|
APP_NAME=Headquarter
|
||||||
|
ROOT_DOMAIN=localhost
|
||||||
|
TOOL_DOMAIN=tools.localhost
|
||||||
|
|
||||||
|
# API / Web URLs
|
||||||
|
API_URL=http://localhost:8000
|
||||||
|
WEB_URL=http://localhost:5173
|
||||||
|
|
||||||
|
# Database (local development)
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
POSTGRES_DB=headquarter
|
||||||
|
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}
|
||||||
|
|
||||||
|
# Authentik OIDC placeholders (wire in FN-004)
|
||||||
|
AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/headquarter/
|
||||||
|
AUTHENTIK_CLIENT_ID=your-client-id
|
||||||
|
AUTHENTIK_CLIENT_SECRET=your-client-secret
|
||||||
|
|
||||||
|
# Traefik / deployment placeholders (wire in FN-006)
|
||||||
|
TRAEFIK_NETWORK=traefik
|
||||||
|
TRAEFIK_ENTRYPOINT=websecure
|
||||||
|
TRAEFIK_CERT_RESOLVER=letsencrypt
|
||||||
|
TOOL_SUBDOMAIN_PATTERN={tool}-{project}-{user}.tools.${ROOT_DOMAIN}
|
||||||
|
|
||||||
|
# Secrets (generate strong random values for production)
|
||||||
|
SECRET_ENCRYPTION_KEY=change-me-in-production
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"globalPause": false,
|
||||||
|
"enginePaused": false,
|
||||||
|
"maxConcurrent": 2,
|
||||||
|
"maxTriageConcurrent": 2,
|
||||||
|
"globalMaxConcurrent": 4,
|
||||||
|
"maxWorktrees": 4,
|
||||||
|
"pollIntervalMs": 15000,
|
||||||
|
"heartbeatMultiplier": 1,
|
||||||
|
"groupOverlappingFiles": true,
|
||||||
|
"overlapIgnorePaths": [],
|
||||||
|
"autoMerge": true,
|
||||||
|
"mergeStrategy": "direct",
|
||||||
|
"directMergeCommitStrategy": "auto",
|
||||||
|
"requirePrApproval": false,
|
||||||
|
"pushAfterMerge": false,
|
||||||
|
"pushRemote": "origin",
|
||||||
|
"unavailableNodePolicy": "block",
|
||||||
|
"recycleWorktrees": false,
|
||||||
|
"executorAllowSiblingBranchRename": false,
|
||||||
|
"worktreeNaming": "random",
|
||||||
|
"taskPrefix": "FN",
|
||||||
|
"includeTaskIdInCommit": true,
|
||||||
|
"commitAuthorEnabled": true,
|
||||||
|
"commitAuthorName": "Fusion",
|
||||||
|
"commitAuthorEmail": "noreply@runfusion.ai",
|
||||||
|
"modelPresets": [],
|
||||||
|
"autoSelectModelPreset": false,
|
||||||
|
"completionDocumentationMode": "off",
|
||||||
|
"defaultPresetBySize": {},
|
||||||
|
"autoResolveConflicts": true,
|
||||||
|
"smartConflictResolution": true,
|
||||||
|
"mergerAutostashMaxAgeHours": 24,
|
||||||
|
"worktreeRebaseBeforeMerge": true,
|
||||||
|
"worktreeRebaseRemote": "",
|
||||||
|
"worktreeRebaseLocalBase": true,
|
||||||
|
"mergeConflictStrategy": "smart-prefer-main",
|
||||||
|
"mergeStrategyOverlapBehavior": "flip-to-prefer-branch",
|
||||||
|
"workflowStepTimeoutMs": 360000,
|
||||||
|
"workflowRevisionForkOnScopeMismatch": true,
|
||||||
|
"strictScopeEnforcement": false,
|
||||||
|
"buildRetryCount": 0,
|
||||||
|
"verificationFixRetries": 3,
|
||||||
|
"buildTimeoutMs": 300000,
|
||||||
|
"requirePlanApproval": false,
|
||||||
|
"agentProvisioning": {},
|
||||||
|
"specStalenessEnabled": false,
|
||||||
|
"specStalenessMaxAgeMs": 21600000,
|
||||||
|
"taskStuckTimeoutMs": 600000,
|
||||||
|
"staleHighFanoutBlockerAgeThresholdMs": 7200000,
|
||||||
|
"aiSessionTtlMs": 604800000,
|
||||||
|
"aiSessionCleanupIntervalMs": 3600000,
|
||||||
|
"autoUnpauseEnabled": true,
|
||||||
|
"autoUnpauseBaseDelayMs": 300000,
|
||||||
|
"autoUnpauseMaxDelayMs": 3600000,
|
||||||
|
"maxStuckKills": 6,
|
||||||
|
"preserveProgressOnStuckRequeue": true,
|
||||||
|
"maxPostReviewFixes": 1,
|
||||||
|
"maxSpawnedAgentsPerParent": 5,
|
||||||
|
"maxSpawnedAgentsGlobal": 20,
|
||||||
|
"maintenanceIntervalMs": 300000,
|
||||||
|
"autoArchiveDoneTasksEnabled": true,
|
||||||
|
"autoArchiveDoneAfterMs": 172800000,
|
||||||
|
"archiveAgentLogMode": "compact",
|
||||||
|
"autoUpdatePrStatus": false,
|
||||||
|
"githubCommentOnDone": false,
|
||||||
|
"githubTrackingEnabledByDefault": false,
|
||||||
|
"githubAuthMode": "gh-cli",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupSchedule": "0 2 * * *",
|
||||||
|
"autoBackupRetention": 7,
|
||||||
|
"autoBackupDir": ".fusion/backups",
|
||||||
|
"memoryBackupEnabled": false,
|
||||||
|
"memoryBackupSchedule": "0 3 * * *",
|
||||||
|
"memoryBackupRetention": 14,
|
||||||
|
"memoryBackupDir": ".fusion/backups/memory",
|
||||||
|
"memoryBackupScope": "all",
|
||||||
|
"autoSummarizeTitles": false,
|
||||||
|
"useAiMergeCommitSummary": true,
|
||||||
|
"insightExtractionEnabled": false,
|
||||||
|
"insightExtractionSchedule": "0 2 * * *",
|
||||||
|
"insightExtractionMinIntervalMs": 86400000,
|
||||||
|
"taskEvaluationEnabled": false,
|
||||||
|
"taskEvaluationSchedule": "0 5 * * *",
|
||||||
|
"taskEvaluationFollowUpPolicy": "off",
|
||||||
|
"memoryEnabled": true,
|
||||||
|
"memoryBackendType": "qmd",
|
||||||
|
"memoryAutoSummarizeEnabled": false,
|
||||||
|
"memoryAutoSummarizeThresholdChars": 50000,
|
||||||
|
"memoryAutoSummarizeSchedule": "0 3 * * *",
|
||||||
|
"memoryDreamsEnabled": false,
|
||||||
|
"memoryDreamsSchedule": "0 4 * * *",
|
||||||
|
"runStepsInNewSessions": false,
|
||||||
|
"maxParallelSteps": 2,
|
||||||
|
"missionStaleThresholdMs": 600000,
|
||||||
|
"missionMaxTaskRetries": 3,
|
||||||
|
"missionHealthCheckIntervalMs": 300000,
|
||||||
|
"reflectionEnabled": false,
|
||||||
|
"reflectionIntervalMs": 3600000,
|
||||||
|
"reflectionAfterTask": true,
|
||||||
|
"reviewHandoffPolicy": "disabled",
|
||||||
|
"showQuickChatFAB": false,
|
||||||
|
"researchSettings": {
|
||||||
|
"enabled": true,
|
||||||
|
"enabledSources": {
|
||||||
|
"webSearch": true,
|
||||||
|
"pageFetch": true,
|
||||||
|
"github": false,
|
||||||
|
"localDocs": true,
|
||||||
|
"llmSynthesis": true
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"maxConcurrentRuns": 3,
|
||||||
|
"maxSourcesPerRun": 20,
|
||||||
|
"maxDurationMs": 300000,
|
||||||
|
"requestTimeoutMs": 30000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"evalSettings": {
|
||||||
|
"enabled": false,
|
||||||
|
"intervalMs": 86400000,
|
||||||
|
"followUpPolicy": "suggest-only",
|
||||||
|
"retentionDays": 30
|
||||||
|
},
|
||||||
|
"researchEnabled": true,
|
||||||
|
"researchMaxConcurrentRuns": 3,
|
||||||
|
"researchDefaultTimeout": 300000,
|
||||||
|
"researchMaxSourcesPerRun": 20,
|
||||||
|
"researchMaxSynthesisRounds": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
# Daily Memory 2026-05-13
|
||||||
|
|
||||||
|
<!-- Append running observations, open loops, and day-to-day notes here. Promote evergreen facts to MEMORY.md. -->
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Daily Memory 2026-05-14
|
||||||
|
|
||||||
|
<!-- Append running observations, open loops, and day-to-day notes here. Promote evergreen facts to MEMORY.md. -->
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Memory Dreams
|
||||||
|
|
||||||
|
<!-- Periodic synthesized patterns from daily notes. Promote durable lessons to MEMORY.md. -->
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Project Memory
|
||||||
|
|
||||||
|
<!-- This file stores durable project learnings. Agents consult and update it during triage and execution. -->
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
<!-- Key architectural patterns, module boundaries, and design decisions -->
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
<!-- Project-specific coding standards, naming patterns, file organization -->
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
<!-- Known issues, common mistakes, and things to avoid -->
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
<!-- Important background information, dependency constraints, deployment notes -->
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.egg-info/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
docker-volumes/
|
||||||
|
|
||||||
|
# Fusion internals
|
||||||
|
.fusion/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.cache/
|
||||||
|
.temp/
|
||||||
|
tmp/
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.PHONY: install lint test typecheck build dev compose-up compose-down clean
|
||||||
|
|
||||||
|
install:
|
||||||
|
pnpm install
|
||||||
|
cd apps/api && python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
||||||
|
|
||||||
|
lint:
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
test:
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
pnpm typecheck
|
||||||
|
|
||||||
|
build:
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
dev:
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
compose-up:
|
||||||
|
docker compose up --build -d
|
||||||
|
|
||||||
|
compose-down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf node_modules apps/web/node_modules apps/api/.venv
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Headquarter
|
||||||
|
|
||||||
|
Hosted workspace and tool-orchestration platform where authenticated users create projects, connect Git repositories, and spawn self-hosted tools such as RunFusion and code-server.
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
This repository is an initial scaffold (FN-002). It provides:
|
||||||
|
|
||||||
|
- React + Vite + TypeScript frontend (`apps/web`)
|
||||||
|
- FastAPI + Python backend (`apps/api`)
|
||||||
|
- Root monorepo tooling (pnpm workspace, Makefile)
|
||||||
|
- Docker Compose local development stack
|
||||||
|
- Deployment skeleton for Portainer + Traefik
|
||||||
|
- Automated tests (Vitest + pytest)
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # React frontend
|
||||||
|
│ └── api/ # FastAPI backend
|
||||||
|
├── docs/ # Architecture, development, and deployment docs
|
||||||
|
├── deploy/ # Portainer/Traefik deployment examples
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── docker-compose.traefik.yml
|
||||||
|
├── package.json # Root monorepo scripts
|
||||||
|
├── Makefile # Common local workflows
|
||||||
|
└── .env.example # Shared environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js ≥ 20 and pnpm ≥ 9
|
||||||
|
- Python ≥ 3.11
|
||||||
|
- Docker and Docker Compose (optional, for local Postgres)
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Copy environment examples
|
||||||
|
cp .env.example .env
|
||||||
|
cp apps/web/.env.example apps/web/.env
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Start frontend and backend in development mode
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the API, web frontend, and PostgreSQL.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `make install` | Install Node and Python dependencies |
|
||||||
|
| `make dev` | Start frontend and backend in parallel |
|
||||||
|
| `make test` | Run frontend and backend tests |
|
||||||
|
| `make lint` | Run linters |
|
||||||
|
| `make typecheck` | Run type checkers |
|
||||||
|
| `make build` | Build frontend and backend |
|
||||||
|
| `make compose-up` | Start Docker Compose stack |
|
||||||
|
| `make compose-down` | Stop Docker Compose stack |
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Architecture](docs/architecture.md) — System design and MVP phases
|
||||||
|
- [Development](docs/development.md) — Local setup and day-to-day commands
|
||||||
|
- [Deployment](docs/deployment.md) — Portainer/Traefik assumptions
|
||||||
|
- [Deploy Skeleton](deploy/README.md) — Deployment file reference
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
This scaffold intentionally defers detailed implementation to follow-up tasks:
|
||||||
|
|
||||||
|
- **FN-004** — Backend domain models, database migrations, API endpoints, auth integration
|
||||||
|
- **FN-005** — Frontend dashboard navigation, project creation, authenticated flows
|
||||||
|
- **FN-006** — Full deployment automation, dynamic Traefik labels for spawned tool containers
|
||||||
|
- **FN-003** — Manifest-driven tool registry
|
||||||
|
- **FN-007** — Provider-independent Git connection model
|
||||||
|
- **FN-008** — RunFusion executable environment proof of concept
|
||||||
|
- **FN-009** — Persistent config and secrets handling
|
||||||
|
- **FN-010** — code-server manifest and spawn flow
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
TBD
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.git/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
COPY app/ ./app/
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
RUN pip install --no-cache-dir -e ".[dev]"
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
app_name: str = "Headquarter API"
|
||||||
|
debug: bool = False
|
||||||
|
api_v1_prefix: str = "/api/v1"
|
||||||
|
|
||||||
|
# Authentik OIDC placeholders (to be wired in FN-004)
|
||||||
|
authentik_issuer_url: str = ""
|
||||||
|
authentik_client_id: str = ""
|
||||||
|
authentik_client_secret: str = ""
|
||||||
|
|
||||||
|
# Database (to be wired in FN-004)
|
||||||
|
database_url: str = "postgresql://postgres:postgres@localhost:5432/headquarter"
|
||||||
|
|
||||||
|
# Deployment
|
||||||
|
root_domain: str = "localhost"
|
||||||
|
tool_subdomain_pattern: str = "{tool}-{project}-{user}.tools.{root_domain}"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title=settings.app_name,
|
||||||
|
debug=settings.debug,
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["http://localhost:5173"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health() -> dict[str, str]:
|
||||||
|
return {"status": "ok", "service": settings.app_name}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "@headquarter/api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": ".venv/bin/uvicorn app.main:app --reload --port 8000",
|
||||||
|
"build": "echo 'Python build step skipped (no compilation required)'",
|
||||||
|
"lint": ".venv/bin/ruff check app tests",
|
||||||
|
"test": ".venv/bin/pytest",
|
||||||
|
"typecheck": ".venv/bin/mypy app tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
[project]
|
||||||
|
name = "headquarter-api"
|
||||||
|
version = "0.0.1"
|
||||||
|
description = "Headquarter FastAPI backend"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.115.0",
|
||||||
|
"uvicorn[standard]>=0.34.0",
|
||||||
|
"pydantic-settings>=2.8.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.3.0",
|
||||||
|
"httpx>=0.28.0",
|
||||||
|
"ruff>=0.11.0",
|
||||||
|
"mypy>=1.15.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["app"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py311"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "N", "W", "UP"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.11"
|
||||||
|
strict = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_health_returns_ok() -> None:
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "ok"
|
||||||
|
assert data["service"] == settings.app_name
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.git/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Frontend runtime configuration
|
||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
|
VITE_APP_NAME=Headquarter
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install -g pnpm && pnpm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Headquarter</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://api:8000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@headquarter/web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"test": "vitest run",
|
||||||
|
"typecheck": "tsc -b --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.3.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.4.0",
|
||||||
|
"@eslint/js": "^9.24.0",
|
||||||
|
"eslint": "^9.24.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "~5.7.0",
|
||||||
|
"typescript-eslint": "^8.29.0",
|
||||||
|
"vite": "^6.3.0",
|
||||||
|
"vitest": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
.app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card h2 {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.08);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import './App.css'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<header className="app-header">
|
||||||
|
<h1>Headquarter</h1>
|
||||||
|
<p className="tagline">Hosted workspace and tool-orchestration platform</p>
|
||||||
|
</header>
|
||||||
|
<main className="app-main">
|
||||||
|
<section className="status-card">
|
||||||
|
<h2>Platform Status</h2>
|
||||||
|
<div className="status-row">
|
||||||
|
<span className="status-label">API</span>
|
||||||
|
<span className="status-value" data-testid="api-status">Checking…</span>
|
||||||
|
</div>
|
||||||
|
<div className="status-row">
|
||||||
|
<span className="status-label">Version</span>
|
||||||
|
<span className="status-value">0.0.1</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer className="app-footer">
|
||||||
|
<p>Scaffolded by FN-002</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import App from '../App'
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('renders the platform name', () => {
|
||||||
|
render(<App />)
|
||||||
|
expect(screen.getByText('Headquarter')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the tagline', () => {
|
||||||
|
render(<App />)
|
||||||
|
expect(screen.getByText('Hosted workspace and tool-orchestration platform')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the platform status section', () => {
|
||||||
|
render(<App />)
|
||||||
|
expect(screen.getByText('Platform Status')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('api-status')).toHaveTextContent('Checking…')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the scaffold footer', () => {
|
||||||
|
render(<App />)
|
||||||
|
expect(screen.getByText('Scaffolded by FN-002')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest'
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #0f172a;
|
||||||
|
--fg: #e2e8f0;
|
||||||
|
--accent: #38bdf8;
|
||||||
|
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/// <reference types="vitest/config" />
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: process.env.VITE_API_URL || 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||||
|
setupFiles: ['./src/__tests__/setup.ts'],
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Deploy Skeleton
|
||||||
|
|
||||||
|
This directory contains deployment-focused examples and environment templates for the Headquarter platform.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `portainer.env.example` | Environment variables for a Portainer-managed stack |
|
||||||
|
| `traefik-labels.example.yml` | Reference Traefik labels for services and tool instances |
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- An external **Traefik** reverse proxy is already running on the target Docker host.
|
||||||
|
- Traefik is attached to a Docker network (default name: `traefik`).
|
||||||
|
- The Traefik network is declared as `external: true` in compose overlays.
|
||||||
|
- TLS termination and certificate resolution are handled by Traefik.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Copy `portainer.env.example` to your secrets manager or Portainer environment configuration.
|
||||||
|
2. Fill in all empty values (client IDs, secrets, database password, encryption key).
|
||||||
|
3. Deploy the stack via Portainer using `docker-compose.yml` + `docker-compose.traefik.yml`.
|
||||||
|
4. Tool subdomain labels will be generated dynamically in FN-006.
|
||||||
|
|
||||||
|
## Traefik Network
|
||||||
|
|
||||||
|
Create the Traefik network if it does not exist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
## Follow-up
|
||||||
|
|
||||||
|
Detailed deployment automation, dynamic labels for spawned tool containers, and CI/CD integration are planned in **FN-006**.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Portainer stack environment variables (no secrets committed)
|
||||||
|
# Copy and configure in Portainer UI or your secrets manager.
|
||||||
|
|
||||||
|
APP_NAME=Headquarter
|
||||||
|
ROOT_DOMAIN=example.com
|
||||||
|
TOOL_DOMAIN=tools.example.com
|
||||||
|
API_URL=https://api.example.com
|
||||||
|
WEB_URL=https://example.com
|
||||||
|
|
||||||
|
# Database
|
||||||
|
POSTGRES_USER=headquarter
|
||||||
|
POSTGRES_DB=headquarter
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
|
||||||
|
# Authentik OIDC
|
||||||
|
AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/headquarter/
|
||||||
|
AUTHENTIK_CLIENT_ID=
|
||||||
|
AUTHENTIK_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# Traefik
|
||||||
|
TRAEFIK_NETWORK=traefik
|
||||||
|
TRAEFIK_ENTRYPOINT=websecure
|
||||||
|
TRAEFIK_CERT_RESOLVER=letsencrypt
|
||||||
|
TOOL_SUBDOMAIN_PATTERN={tool}-{project}-{user}.tools.{ROOT_DOMAIN}
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
SECRET_ENCRYPTION_KEY=
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Example Traefik labels for Headquarter services
|
||||||
|
# These labels are applied automatically by docker-compose.traefik.yml.
|
||||||
|
# Use this file as a reference when adding custom tool container labels later.
|
||||||
|
|
||||||
|
# API service labels
|
||||||
|
api_labels: &api
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.headquarter-api.rule=Host(`api.example.com`)"
|
||||||
|
- "traefik.http.routers.headquarter-api.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.headquarter-api.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.headquarter-api.loadbalancer.server.port=8000"
|
||||||
|
|
||||||
|
# Web service labels
|
||||||
|
web_labels: &web
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.headquarter-web.rule=Host(`example.com`)"
|
||||||
|
- "traefik.http.routers.headquarter-web.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.headquarter-web.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.headquarter-web.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
# Tool instance labels (template for dynamically spawned containers)
|
||||||
|
tool_labels: &tool
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.{tool_name}.rule=Host(`{subdomain}`)"
|
||||||
|
- "traefik.http.routers.{tool_name}.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.{tool_name}.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.{tool_name}.loadbalancer.server.port={port}"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- traefik
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.headquarter-api.rule=Host(`api.${ROOT_DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.headquarter-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
|
||||||
|
- "traefik.http.routers.headquarter-api.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}"
|
||||||
|
- "traefik.http.services.headquarter-api.loadbalancer.server.port=8000"
|
||||||
|
|
||||||
|
web:
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- traefik
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.headquarter-web.rule=Host(`${ROOT_DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.headquarter-web.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}"
|
||||||
|
- "traefik.http.routers.headquarter-web.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}"
|
||||||
|
- "traefik.http.services.headquarter-web.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ./apps/api
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-headquarter}
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
volumes:
|
||||||
|
- api-data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ./apps/web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "5173:80"
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
environment:
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER:-postgres}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB:-headquarter}
|
||||||
|
volumes:
|
||||||
|
- postgres-data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-headquarter}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres-data:
|
||||||
|
api-data:
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Headquarter Documentation
|
||||||
|
|
||||||
|
This directory contains architecture, development, and deployment documentation for the Headquarter platform.
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
- [Architecture](architecture.md) — System architecture, stack decisions, and MVP phases *(FN-001)*
|
||||||
|
- [Development](development.md) — Local setup, prerequisites, and day-to-day commands *(FN-002)*
|
||||||
|
- [Deployment](deployment.md) — Deployment assumptions, Portainer/Traefik skeleton, and follow-up scope *(FN-002)*
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [Root README](../README.md)
|
||||||
|
- [Deploy Skeleton](../deploy/README.md)
|
||||||
|
- [Project Brief](project-brief.md) — Original product brief and confirmed stack *(FN-001)*
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Headquarter Architecture
|
||||||
|
|
||||||
|
> Canonical architecture specification for the hosted workspace and tool-orchestration platform.
|
||||||
|
> Decisions in this document override ad-hoc choices in implementation tasks.
|
||||||
|
|
||||||
|
## 1. Overview & Goals
|
||||||
|
|
||||||
|
Headquarter is a hosted control plane where authenticated users create projects, connect Git repositories, and spawn containerized tools (RunFusion, code-server, and future tools). The platform is manifest-driven and provider-abstracted so new tools, Git providers, runtimes, and access providers can be added without rewriting core orchestration logic.
|
||||||
|
|
||||||
|
**MVP scope:** Single-user projects, Authentik OIDC auth, Docker runtime, Traefik subdomain routing, Portainer-managed Docker Compose deployment.
|
||||||
|
|
||||||
|
## 2. Domain Model
|
||||||
|
|
||||||
|
```text
|
||||||
|
User ──< Project ──< Repository
|
||||||
|
└──< ToolInstance
|
||||||
|
|
||||||
|
Tool (manifest-driven, global registry)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **User:** Authentik-managed identity. MVP assumes individual users; schema leaves room for teams/shared projects later.
|
||||||
|
- **Project:** Owned by a user. Contains repositories and spawned tool instances.
|
||||||
|
- **Repository:** Git-backed workspace. Clone, fetch, and push via provider-independent adapters.
|
||||||
|
- **Tool:** Manifest-driven definition (image, ports, mounts, env, health checks, routing rules). Defined in FN-003.
|
||||||
|
- **ToolInstance:** A running container spawned from a Tool manifest for a specific project. Receives workspace mounts, config mounts, secrets, and Traefik routing labels.
|
||||||
|
|
||||||
|
## 3. Provider Interfaces
|
||||||
|
|
||||||
|
The backend must define provider contracts before implementing any concrete adapter.
|
||||||
|
|
||||||
|
### 3.1 GitProvider
|
||||||
|
|
||||||
|
```python
|
||||||
|
class GitProvider(Protocol):
|
||||||
|
def clone(self, repo_url: str, dest: Path, credentials: GitCredentials) -> None: ...
|
||||||
|
def fetch(self, repo_path: Path, credentials: GitCredentials) -> None: ...
|
||||||
|
def push(self, repo_path: Path, credentials: GitCredentials) -> None: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Adapters: GitHub, GitLab, Gitea, Forgejo, etc.
|
||||||
|
- Credentials: generated SSH keys (per-repository) or access tokens.
|
||||||
|
- SSH keys must be scoped per repository connection for clean revocation.
|
||||||
|
|
||||||
|
### 3.2 RuntimeProvider
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RuntimeProvider(Protocol):
|
||||||
|
def spawn(self, manifest: ToolManifest, project: Project, config: SpawnConfig) -> ToolInstance: ...
|
||||||
|
def stop(self, instance: ToolInstance) -> None: ...
|
||||||
|
def health(self, instance: ToolInstance) -> HealthStatus: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- MVP adapter: Docker Compose service generation + Docker API.
|
||||||
|
- Future adapters: Kubernetes, Nomad, etc.
|
||||||
|
|
||||||
|
### 3.3 AccessProvider
|
||||||
|
|
||||||
|
```python
|
||||||
|
class AccessProvider(Protocol):
|
||||||
|
def route(self, instance: ToolInstance, domain: str) -> RoutingConfig: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- MVP adapter: Traefik labels on Docker containers.
|
||||||
|
- Future adapter: Cloudflare Tunnel, etc.
|
||||||
|
|
||||||
|
## 4. Deployment Architecture
|
||||||
|
|
||||||
|
### 4.1 MVP Target
|
||||||
|
|
||||||
|
- **Orchestration:** Portainer-managed Docker Compose stack.
|
||||||
|
- **Reverse Proxy:** Existing Traefik instance (external to the app stack).
|
||||||
|
- **Network:** Shared Traefik Docker network; app stack attaches to it.
|
||||||
|
- **Certificate Resolution:** Let's Encrypt or internal CA via Traefik cert resolver.
|
||||||
|
|
||||||
|
### 4.2 Subdomain Routing
|
||||||
|
|
||||||
|
Path-based routing is avoided because many tools expect to run at `/`.
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
```
|
||||||
|
https://{tool}-{project}-{user}.{tool_domain}
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
|
https://runfusion-myapp-alice.tools.example.com
|
||||||
|
https://code-myapp-alice.tools.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Compose Skeleton
|
||||||
|
|
||||||
|
- `docker-compose.yml`: local development (backend, frontend, PostgreSQL).
|
||||||
|
- `docker-compose.traefik.yml`: deployment overlay with Traefik labels and external network.
|
||||||
|
- Environment-driven; no secrets committed to repository.
|
||||||
|
|
||||||
|
## 5. Security Boundaries
|
||||||
|
|
||||||
|
### 5.1 Authentication
|
||||||
|
|
||||||
|
- Authentik OIDC for user login.
|
||||||
|
- FastAPI backend validates JWT/id tokens at API boundaries.
|
||||||
|
- Frontend stores tokens securely (httpOnly cookie or secure storage pattern).
|
||||||
|
|
||||||
|
### 5.2 Secrets
|
||||||
|
|
||||||
|
- Never treat secrets as plaintext config.
|
||||||
|
- Support encrypted storage at rest and runtime injection as:
|
||||||
|
- Environment variables
|
||||||
|
- Mounted secret files
|
||||||
|
- Encryption key is an environment secret (`SECRET_ENCRYPTION_KEY`).
|
||||||
|
|
||||||
|
### 5.3 SSH Keys
|
||||||
|
|
||||||
|
- Generated per repository connection.
|
||||||
|
- Stored encrypted.
|
||||||
|
- Injected into tool containers at runtime for Git operations.
|
||||||
|
|
||||||
|
### 5.4 Container Isolation
|
||||||
|
|
||||||
|
- Each tool instance runs in its own container.
|
||||||
|
- Resource limits declared in tool manifest.
|
||||||
|
- Workspace and config mounts are scoped to user/project.
|
||||||
|
|
||||||
|
## 6. Data & Storage
|
||||||
|
|
||||||
|
### 6.1 Database
|
||||||
|
|
||||||
|
- PostgreSQL for relational data (users, projects, repositories, tool instances, manifests).
|
||||||
|
- Schema migrations managed by backend (Alembic or equivalent).
|
||||||
|
|
||||||
|
### 6.2 Filesystem Layout (Conceptual)
|
||||||
|
|
||||||
|
```text
|
||||||
|
/data/
|
||||||
|
users/{userId}/tool-configs/{toolId}/
|
||||||
|
projects/{projectId}/repo/
|
||||||
|
projects/{projectId}/tool-configs/{toolId}/
|
||||||
|
```
|
||||||
|
|
||||||
|
- Repository workspace storage: Docker volumes or local bind mounts.
|
||||||
|
- Tool config storage: persistent host mounts, separate from repository workspaces.
|
||||||
|
- Config scopes: global default → user-level → project-level → tool-instance override.
|
||||||
|
|
||||||
|
## 7. Tool Manifest & Orchestration
|
||||||
|
|
||||||
|
Tools are defined by manifests (FN-003) that declare:
|
||||||
|
|
||||||
|
- Runtime image / image tag
|
||||||
|
- Node/npm version expectations (for executable environments)
|
||||||
|
- Bootstrap / install commands
|
||||||
|
- Command execution needs
|
||||||
|
- Workspace mounts
|
||||||
|
- Config mounts
|
||||||
|
- Environment variables
|
||||||
|
- Secrets
|
||||||
|
- Ports
|
||||||
|
- Health checks
|
||||||
|
- Resource limits
|
||||||
|
- Traefik routing needs (subdomain pattern, middleware)
|
||||||
|
|
||||||
|
The platform reads manifests and generates:
|
||||||
|
- Docker Compose service definitions
|
||||||
|
- Traefik labels for routing
|
||||||
|
- Volume mounts for workspace and config
|
||||||
|
- Secret injection at runtime
|
||||||
|
|
||||||
|
## 8. MVP Phases
|
||||||
|
|
||||||
|
| Phase | Task | Deliverable |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| Foundation | FN-002 | Monorepo scaffold, build/test/lint pipelines |
|
||||||
|
| Registry | FN-003 | Manifest schema, RunFusion and code-server manifests |
|
||||||
|
| Backend | FN-004 | FastAPI app, domain models, API endpoints, DB migrations |
|
||||||
|
| Frontend | FN-005 | Auth-ready shell, navigation, placeholder screens |
|
||||||
|
| Deployment | FN-006 | Docker Compose overlays, Traefik labels, Portainer config |
|
||||||
|
| Git Model | FN-007 | Provider interface, SSH key generation, credential storage |
|
||||||
|
| RunFusion POC | FN-008 | Executable environment proof of concept |
|
||||||
|
| Secrets & Config | FN-009 | Encrypted secrets, persistent config mounts |
|
||||||
|
| code-server Spawn | FN-010 | code-server manifest, spawn script, runtime integration |
|
||||||
|
|
||||||
|
## 9. Extension Points
|
||||||
|
|
||||||
|
- **New Git providers:** Implement `GitProvider` protocol.
|
||||||
|
- **New tools:** Add a manifest to the registry (no code changes required for standard containers).
|
||||||
|
- **New runtimes:** Implement `RuntimeProvider` protocol.
|
||||||
|
- **New access providers:** Implement `AccessProvider` protocol.
|
||||||
|
- **Teams/organizations:** Add `Organization` and `ProjectMember` entities later.
|
||||||
|
|
||||||
|
## 10. Environment Assumptions
|
||||||
|
|
||||||
|
Required environment variables (no defaults in production):
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_NAME=
|
||||||
|
ROOT_DOMAIN=
|
||||||
|
TOOL_DOMAIN=
|
||||||
|
API_URL=
|
||||||
|
TRAEFIK_NETWORK=
|
||||||
|
TRAEFIK_ENTRYPOINT=
|
||||||
|
TRAEFIK_CERT_RESOLVER=
|
||||||
|
AUTHENTIK_ISSUER_URL=
|
||||||
|
AUTHENTIK_CLIENT_ID=
|
||||||
|
AUTHENTIK_CLIENT_SECRET=
|
||||||
|
DATABASE_URL=
|
||||||
|
SECRET_ENCRYPTION_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
Local development uses `.env.example` and safe defaults.
|
||||||
|
|
||||||
|
## 11. Technology Boundaries
|
||||||
|
|
||||||
|
| Layer | Choice | Migration Path |
|
||||||
|
|-------|--------|----------------|
|
||||||
|
| Frontend | React + Vite | Next.js, Vue, etc. if needed |
|
||||||
|
| Backend | FastAPI | Any ASGI framework |
|
||||||
|
| Database | PostgreSQL | Managed Postgres, CockroachDB |
|
||||||
|
| Runtime | Docker Compose | Kubernetes, Nomad |
|
||||||
|
| Access | Traefik | Cloudflare Tunnel, custom proxy |
|
||||||
|
| Auth | Authentik OIDC | Any OIDC provider |
|
||||||
|
|
||||||
|
## 12. Acceptance Criteria for Architecture Compliance
|
||||||
|
|
||||||
|
Any implementation task must:
|
||||||
|
|
||||||
|
1. Respect provider interfaces (no hardcoded GitHub/Traefik logic in core orchestration).
|
||||||
|
2. Keep secrets out of committed files and plaintext logs.
|
||||||
|
3. Use environment variables for deployment-specific values.
|
||||||
|
4. Leave schema room for multi-user teams without rewriting ownership models.
|
||||||
|
5. Support adding a new tool via manifest + registry entry alone (no new backend code for standard containers).
|
||||||
|
|
||||||
|
## 13. Deferred Decisions
|
||||||
|
|
||||||
|
- **Multi-tenancy:** MVP is single-tenant deployment. Multi-tenant routing and isolation are future concerns.
|
||||||
|
- **High availability:** No replicas or load balancing in MVP.
|
||||||
|
- **Backup strategy:** Out of MVP scope; rely on host-level volume backups.
|
||||||
|
- **Rate limiting:** Not in MVP; add at Traefik or API gateway layer later.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Deployment Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The MVP deployment target is a **Portainer-managed Docker Compose stack** with an existing **Traefik** reverse proxy.
|
||||||
|
|
||||||
|
This document covers the scaffold-level deployment assumptions created in FN-002. Detailed deployment automation (dynamic labels for spawned tool containers, secret rotation, CI/CD pipelines) is follow-up scope for **FN-006**.
|
||||||
|
|
||||||
|
## Stack Assumptions
|
||||||
|
|
||||||
|
- **Reverse proxy**: Traefik (already running on the target host)
|
||||||
|
- **Orchestration**: Portainer managing Docker Compose stacks
|
||||||
|
- **Network**: External Traefik network named `traefik` (or as configured)
|
||||||
|
- **Routing**: Subdomain-based (`{tool}-{project}-{user}.tools.{ROOT_DOMAIN}`)
|
||||||
|
- **TLS**: Traefik cert resolver (e.g., `letsencrypt` or Cloudflare)
|
||||||
|
|
||||||
|
## Deployment Files
|
||||||
|
|
||||||
|
The following deployment files are part of the scaffold:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `docker-compose.yml` | Local development (API, web, Postgres) |
|
||||||
|
| `docker-compose.traefik.yml` | Deployment overlay with Traefik labels |
|
||||||
|
| `deploy/portainer.env.example` | Deployment environment variables |
|
||||||
|
| `deploy/traefik-labels.example.yml` | Example Traefik labels for services |
|
||||||
|
| `deploy/README.md` | Deploy skeleton usage notes |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
See `.env.example` for the full variable list. Key deployment variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_NAME=Headquarter
|
||||||
|
ROOT_DOMAIN=example.com
|
||||||
|
TOOL_DOMAIN=tools.example.com
|
||||||
|
TRAEFIK_NETWORK=traefik
|
||||||
|
TRAEFIK_ENTRYPOINT=websecure
|
||||||
|
TRAEFIK_CERT_RESOLVER=letsencrypt
|
||||||
|
AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/headquarter/
|
||||||
|
AUTHENTIK_CLIENT_ID=
|
||||||
|
AUTHENTIK_CLIENT_SECRET=
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
SECRET_ENCRYPTION_KEY=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local vs Production
|
||||||
|
|
||||||
|
- **Local**: `docker compose up --build -d` uses `docker-compose.yml` only.
|
||||||
|
- **Production**: Portainer deploys the stack using the main compose file plus the Traefik overlay.
|
||||||
|
|
||||||
|
## Scoped Secrets
|
||||||
|
|
||||||
|
Do not commit real secrets. Use:
|
||||||
|
|
||||||
|
- Portainer environment variables (stored in Portainer, not in Git)
|
||||||
|
- `.env` files (ignored by Git, documented in `.env.example`)
|
||||||
|
- Docker secrets (to be evaluated in FN-006)
|
||||||
|
|
||||||
|
## Follow-up Work
|
||||||
|
|
||||||
|
- **FN-006**: Full deployment automation, dynamic Traefik labels for spawned tool containers, Portainer stack definitions, and CI/CD integration.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# Development Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Node.js** ≥ 20 and **pnpm** ≥ 9
|
||||||
|
- **Python** ≥ 3.11 with `venv` support
|
||||||
|
- **Docker** and **Docker Compose** (for local services)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Node dependencies and Python virtualenv + packages
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Or manually:
|
||||||
|
pnpm install
|
||||||
|
cd apps/api && python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Setup
|
||||||
|
|
||||||
|
Copy the root environment example and fill in local values:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy the frontend environment example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp apps/web/.env.example apps/web/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Locally
|
||||||
|
|
||||||
|
### Frontend only
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/web
|
||||||
|
pnpm dev # Vite dev server on http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend only
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/api
|
||||||
|
.venv/bin/uvicorn app.main:app --reload --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Both (via root script)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev # Runs frontend and backend in parallel
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @headquarter/web test
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses **Vitest** + **Testing Library** + **jsdom**.
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @headquarter/api test
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly with pytest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/api && .venv/bin/pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### All tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
# or
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linting and Type Checking
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @headquarter/web lint
|
||||||
|
pnpm --filter @headquarter/web typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @headquarter/api lint
|
||||||
|
pnpm --filter @headquarter/api typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
### All
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make lint
|
||||||
|
make typecheck
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
# or
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Vite React TypeScript frontend
|
||||||
|
│ └── api/ # FastAPI Python backend
|
||||||
|
├── docs/ # Documentation
|
||||||
|
├── deploy/ # Deployment skeleton files
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── package.json # Root monorepo scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Frontend**: React functional components, TypeScript strict mode, ESLint + Ruff-like rules.
|
||||||
|
- **Backend**: FastAPI, Pydantic settings, pytest, ruff, mypy.
|
||||||
|
- **Commits**: Conventional commits with task ID prefix, e.g. `feat(FN-002): description`.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "headquarter",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"description": "Hosted workspace and tool-orchestration platform",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "pnpm --filter @headquarter/web lint && pnpm --filter @headquarter/api lint",
|
||||||
|
"test": "pnpm --filter @headquarter/web test && pnpm --filter @headquarter/api test",
|
||||||
|
"typecheck": "pnpm --filter @headquarter/web typecheck && pnpm --filter @headquarter/api typecheck",
|
||||||
|
"build": "pnpm --filter @headquarter/web build && pnpm --filter @headquarter/api build",
|
||||||
|
"dev": "pnpm --parallel --filter @headquarter/web --filter @headquarter/api dev",
|
||||||
|
"compose:up": "docker compose up --build -d",
|
||||||
|
"compose:down": "docker compose down"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@11.1.1",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20",
|
||||||
|
"pnpm": ">=9"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2864
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
packages:
|
||||||
|
- 'apps/*'
|
||||||
|
- 'packages/*'
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: set this to true or false
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
Reference in New Issue
Block a user