commit c0b013f0677678aa0b16904aa0029428947426f1 Author: Pavel Kerndl Date: Mon Jul 27 18:27:37 2026 +0200 Initialize Aidoit v0.1 monorepo diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..64d3bf0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5852d8a --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +AIDOIT_ENV=development +AIDOIT_ADMIN_TOKEN=replace-with-a-long-random-token + +POSTGRES_DB=aidoit +POSTGRES_USER=aidoit +POSTGRES_PASSWORD=replace-me +DATABASE_URL=postgresql+psycopg://aidoit:replace-me@localhost:5432/aidoit + +REDIS_URL=redis://localhost:6379/0 + +MINIO_ROOT_USER=aidoit +MINIO_ROOT_PASSWORD=replace-with-a-long-password +MINIO_ENDPOINT=http://localhost:9000 +MINIO_BUCKET=aidoit-artifacts + +NEXT_PUBLIC_API_URL=http://localhost:8000 +WEB_ORIGIN=http://localhost:3000 +MSP_ORIGIN=http://localhost:3001 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..ff0addb --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + +jobs: + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --no-frozen-lockfile + - run: pnpm typecheck + - run: pnpm build + + api: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: pip install -r apps/api/requirements-dev.txt + - run: pytest apps/api/tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6e6f60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.env +.env.local +.env.*.local +.DS_Store +.idea/ +.vscode/ +node_modules/ +.pnpm-store/ +.turbo/ +.next/ +dist/ +build/ +coverage/ +*.log +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.venv/ +venv/ +target/ +*.db +*.sqlite3 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c2774c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +.PHONY: install dev up down logs build lint typecheck test + +install: + pnpm install + cd apps/api && python -m venv .venv && .venv/bin/pip install -r requirements-dev.txt + +dev: + pnpm dev + +up: + docker compose -f infrastructure/docker/compose.yml up -d + +down: + docker compose -f infrastructure/docker/compose.yml down + +logs: + docker compose -f infrastructure/docker/compose.yml logs -f + +build: + pnpm build + +lint: + pnpm lint + +typecheck: + pnpm typecheck + +test: + pnpm test diff --git a/README.md b/README.md new file mode 100644 index 0000000..b47f126 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# Aidoit + +Aidoit is a single-user personal AI operating system for building applications, +running AI teams, and managing infrastructure. + +## Repository structure + +```text +apps/ + web/ Main web development dashboard + msp/ Infrastructure management dashboard + api/ Shared modular REST API + desktop/ Tauri desktop application workspace + +packages/ + ui/ Shared React components and design tokens + contracts/ Shared domain and API contracts + sdk/ Typed API client + config/ Shared TypeScript configuration + +infrastructure/ + docker/ Local Docker Compose stack + nginx/ Reverse-proxy examples + +docs/ + architecture/ + decisions/ + roadmap/ +``` + +## Prerequisites + +- Node.js 22+ +- pnpm 10+ +- Python 3.13+ +- Docker with Docker Compose + +## Local development + +```bash +cp .env.example .env +pnpm install +docker compose -f infrastructure/docker/compose.yml up -d postgres redis minio +pnpm dev +``` + +Services: + +- Web dashboard: http://localhost:3000 +- MSP dashboard: http://localhost:3001 +- API: http://localhost:8000 +- OpenAPI: http://localhost:8000/docs +- MinIO console: http://localhost:9001 + +## First production targets + +- `web.aidoit.top` +- `msp.aidoit.top` +- `api.aidoit.top` +- desktop application communicating with the shared API + +## Status + +This repository is the professional v0.1 foundation. It intentionally starts as a +modular monolith and leaves room for later extraction of workers or services when +real operational needs justify it. diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..bf8b248 --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.13-slim AS runtime + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY pyproject.toml requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/apps/api/app/__init__.py b/apps/api/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/core/__init__.py b/apps/api/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/core/config.py b/apps/api/app/core/config.py new file mode 100644 index 0000000..15535d5 --- /dev/null +++ b/apps/api/app/core/config.py @@ -0,0 +1,20 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + aidoit_env: str = "development" + aidoit_admin_token: str = "replace-me" + database_url: str = "sqlite:///./aidoit.db" + redis_url: str = "redis://localhost:6379/0" + web_origin: str = "http://localhost:3000" + msp_origin: str = "http://localhost:3001" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore", + ) + + +settings = Settings() diff --git a/apps/api/app/main.py b/apps/api/app/main.py new file mode 100644 index 0000000..730e56a --- /dev/null +++ b/apps/api/app/main.py @@ -0,0 +1,32 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.core.config import settings +from app.modules.health.router import router as health_router +from app.modules.projects.router import router as projects_router + +app = FastAPI( + title="Aidoit API", + version="0.1.0", + description="Shared REST API for the Aidoit personal AI operating system.", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=[settings.web_origin, settings.msp_origin], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(health_router) +app.include_router(projects_router, prefix="/api/v1") + + +@app.get("/") +async def root() -> dict[str, str]: + return { + "name": "Aidoit API", + "version": "0.1.0", + "docs": "/docs", + } diff --git a/apps/api/app/modules/__init__.py b/apps/api/app/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/artifacts/.gitkeep b/apps/api/app/modules/artifacts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/auth/.gitkeep b/apps/api/app/modules/auth/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/deployments/.gitkeep b/apps/api/app/modules/deployments/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/health/__init__.py b/apps/api/app/modules/health/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/health/router.py b/apps/api/app/modules/health/router.py new file mode 100644 index 0000000..6782ca0 --- /dev/null +++ b/apps/api/app/modules/health/router.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + +router = APIRouter(tags=["health"]) + + +@router.get("/health") +async def health() -> dict[str, str]: + return { + "status": "ok", + "service": "aidoit-api", + } diff --git a/apps/api/app/modules/projects/__init__.py b/apps/api/app/modules/projects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/projects/router.py b/apps/api/app/modules/projects/router.py new file mode 100644 index 0000000..f341c96 --- /dev/null +++ b/apps/api/app/modules/projects/router.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter, HTTPException, status + +from app.modules.projects.schemas import ProjectCreate, ProjectRead +from app.modules.projects.service import project_service + +router = APIRouter(prefix="/projects", tags=["projects"]) + + +@router.get("", response_model=list[ProjectRead]) +async def list_projects() -> list[ProjectRead]: + return project_service.list() + + +@router.post( + "", + response_model=ProjectRead, + status_code=status.HTTP_201_CREATED, +) +async def create_project(payload: ProjectCreate) -> ProjectRead: + return project_service.create(payload) + + +@router.get("/{project_id}", response_model=ProjectRead) +async def get_project(project_id: int) -> ProjectRead: + project = project_service.get(project_id) + if project is None: + raise HTTPException(status_code=404, detail="Project not found") + return project diff --git a/apps/api/app/modules/projects/schemas.py b/apps/api/app/modules/projects/schemas.py new file mode 100644 index 0000000..4b76785 --- /dev/null +++ b/apps/api/app/modules/projects/schemas.py @@ -0,0 +1,19 @@ +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + +ProductType = Literal["web", "desktop", "msp"] + + +class ProjectCreate(BaseModel): + name: str = Field(min_length=1, max_length=120) + description: str = Field(default="", max_length=2000) + product: ProductType = "web" + + +class ProjectRead(ProjectCreate): + model_config = ConfigDict(from_attributes=True) + + id: int + created_at: datetime diff --git a/apps/api/app/modules/projects/service.py b/apps/api/app/modules/projects/service.py new file mode 100644 index 0000000..07e701d --- /dev/null +++ b/apps/api/app/modules/projects/service.py @@ -0,0 +1,31 @@ +from datetime import UTC, datetime +from itertools import count + +from app.modules.projects.schemas import ProjectCreate, ProjectRead + + +class ProjectService: + def __init__(self) -> None: + self._ids = count(1) + self._projects: list[ProjectRead] = [] + + def list(self) -> list[ProjectRead]: + return self._projects.copy() + + def create(self, payload: ProjectCreate) -> ProjectRead: + project = ProjectRead( + id=next(self._ids), + created_at=datetime.now(UTC), + **payload.model_dump(), + ) + self._projects.append(project) + return project + + def get(self, project_id: int) -> ProjectRead | None: + return next( + (project for project in self._projects if project.id == project_id), + None, + ) + + +project_service = ProjectService() diff --git a/apps/api/app/modules/runs/.gitkeep b/apps/api/app/modules/runs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/tasks/.gitkeep b/apps/api/app/modules/tasks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/teams/.gitkeep b/apps/api/app/modules/teams/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/app/modules/tools/.gitkeep b/apps/api/app/modules/tools/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml new file mode 100644 index 0000000..6be90dd --- /dev/null +++ b/apps/api/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "aidoit-api" +version = "0.1.0" +description = "Shared REST API for Aidoit" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.115", + "uvicorn[standard]>=0.34", + "pydantic-settings>=2.7", + "sqlalchemy>=2.0", + "psycopg[binary]>=3.2", + "alembic>=1.14", + "redis>=5.2", + "httpx>=0.28" +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.3", + "pytest-asyncio>=0.25", + "ruff>=0.9", + "mypy>=1.14" +] + +[tool.ruff] +line-length = 100 +target-version = "py313" + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/apps/api/requirements-dev.txt b/apps/api/requirements-dev.txt new file mode 100644 index 0000000..aefbcb6 --- /dev/null +++ b/apps/api/requirements-dev.txt @@ -0,0 +1 @@ +-e .[dev] diff --git a/apps/api/requirements.txt b/apps/api/requirements.txt new file mode 100644 index 0000000..d6e1198 --- /dev/null +++ b/apps/api/requirements.txt @@ -0,0 +1 @@ +-e . diff --git a/apps/api/tests/test_health.py b/apps/api/tests/test_health.py new file mode 100644 index 0000000..e2ec142 --- /dev/null +++ b/apps/api/tests/test_health.py @@ -0,0 +1,11 @@ +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_health() -> None: + response = client.get("/health") + assert response.status_code == 200 + assert response.json()["status"] == "ok" diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 0000000..6a13d0f --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,16 @@ +# Aidoit Desktop + +This directory is reserved for the native Tauri application. + +Planned capabilities: + +- API authentication and synchronization +- local filesystem access +- shell execution +- Git operations +- Docker operations +- local Ollama integration +- native builds for macOS, Windows and Linux + +Tauri initialization is intentionally deferred until the shared contracts and +desktop security boundaries are finalized. diff --git a/apps/msp/app/layout.tsx b/apps/msp/app/layout.tsx new file mode 100644 index 0000000..8d1e436 --- /dev/null +++ b/apps/msp/app/layout.tsx @@ -0,0 +1,16 @@ +import "@aidoit/ui/styles.css"; + +export const metadata = { + title: "Aidoit MSP", + description: "Infrastructure management workspace", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/apps/msp/app/page.tsx b/apps/msp/app/page.tsx new file mode 100644 index 0000000..ea62063 --- /dev/null +++ b/apps/msp/app/page.tsx @@ -0,0 +1,11 @@ +export default function MspHomePage() { + return ( +
+

AIDOIT MSP

+

Infrastructure workspace

+

+ VPS, Proxmox, Docker, LXC, monitoring, backups and deployments will live here. +

+
+ ); +} diff --git a/apps/msp/next.config.ts b/apps/msp/next.config.ts new file mode 100644 index 0000000..dbf57ed --- /dev/null +++ b/apps/msp/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const config: NextConfig = { + output: "standalone", + transpilePackages: ["@aidoit/ui", "@aidoit/contracts", "@aidoit/sdk"], +}; + +export default config; diff --git a/apps/msp/package.json b/apps/msp/package.json new file mode 100644 index 0000000..a6f1740 --- /dev/null +++ b/apps/msp/package.json @@ -0,0 +1,28 @@ +{ + "name": "@aidoit/msp", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --port 3001", + "build": "next build", + "start": "next start --port 3001", + "lint": "next lint", + "typecheck": "tsc --noEmit", + "test": "echo \"No MSP tests yet\"" + }, + "dependencies": { + "@aidoit/contracts": "workspace:*", + "@aidoit/sdk": "workspace:*", + "@aidoit/ui": "workspace:*", + "next": "^15.2.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@aidoit/config": "workspace:*", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.8.0" + } +} \ No newline at end of file diff --git a/apps/msp/tsconfig.json b/apps/msp/tsconfig.json new file mode 100644 index 0000000..8c876a2 --- /dev/null +++ b/apps/msp/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@aidoit/config/nextjs.json", + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/web/app/app.css b/apps/web/app/app.css new file mode 100644 index 0000000..bd43244 --- /dev/null +++ b/apps/web/app/app.css @@ -0,0 +1,218 @@ +.app-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 250px 1fr; +} + +.sidebar { + padding: 26px 20px; + border-right: 1px solid var(--border); + background: rgba(6, 14, 22, 0.9); + display: flex; + flex-direction: column; +} + +.brand { + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 34px; +} + +.brand strong, +.brand span { + display: block; +} + +.brand span { + margin-top: 3px; + color: var(--muted); + font-size: 12px; +} + +.brand-mark { + width: 40px; + height: 40px; + border-radius: 13px; + display: grid; + place-items: center; + color: #041019; + background: linear-gradient(135deg, #83dcff, #4ca3ff); + font-weight: 900; +} + +nav { + display: grid; + gap: 6px; +} + +nav a { + color: var(--muted); + padding: 11px 12px; + border-radius: 10px; + text-decoration: none; +} + +nav a:hover, +nav a.active { + color: var(--text); + background: rgba(87, 199, 255, 0.1); +} + +.api-status { + margin-top: auto; + color: var(--muted); + font-size: 12px; + border-top: 1px solid var(--border); + padding-top: 18px; +} + +.api-status span, +.feed-item > span { + display: inline-block; + width: 8px; + height: 8px; + margin-right: 7px; + border-radius: 50%; + background: #5be18a; +} + +.workspace { + padding: 38px; +} + +.hero { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 20px; + margin-bottom: 28px; +} + +.hero h1 { + font-size: clamp(30px, 4vw, 48px); + margin: 3px 0 10px; + letter-spacing: -0.04em; +} + +.hero > div > p:last-child, +.empty-state p, +.feed-item p { + color: var(--muted); +} + +.hero button { + border: 0; + border-radius: 12px; + padding: 13px 18px; + background: var(--accent); + color: #041019; + font-weight: 800; + cursor: pointer; +} + +.eyebrow { + margin: 0; + color: var(--accent); + font-weight: 800; + font-size: 11px; + letter-spacing: 0.14em; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + margin-bottom: 16px; +} + +.content-grid { + display: grid; + grid-template-columns: 1.4fr 0.8fr; + gap: 16px; +} + +.panel { + min-height: 330px; + padding: 22px; + border: 1px solid var(--border); + background: linear-gradient(180deg, rgba(16, 30, 44, 0.96), rgba(10, 22, 32, 0.96)); + border-radius: 18px; +} + +.panel h2 { + margin: 3px 0 0; +} + +.panel-heading { + display: flex; + justify-content: space-between; + align-items: center; +} + +.panel-heading a { + color: var(--accent); + text-decoration: none; + font-size: 13px; +} + +.empty-state { + min-height: 240px; + display: grid; + place-items: center; + align-content: center; + text-align: center; +} + +.empty-state > div { + width: 54px; + height: 54px; + border-radius: 18px; + display: grid; + place-items: center; + background: rgba(87, 199, 255, 0.1); + color: var(--accent); + font-size: 28px; +} + +.feed-item { + margin-top: 30px; + display: flex; + align-items: flex-start; +} + +.feed-item p { + margin-top: 6px; +} + +@media (max-width: 1000px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + display: none; + } + + .stats-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .content-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 620px) { + .workspace { + padding: 22px; + } + + .hero { + display: grid; + } + + .stats-grid { + grid-template-columns: 1fr; + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 0000000..f01f5ea --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import "@aidoit/ui/styles.css"; +import "./app.css"; + +export const metadata: Metadata = { + title: "Aidoit", + description: "Personal AI operating system", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx new file mode 100644 index 0000000..750029b --- /dev/null +++ b/apps/web/app/page.tsx @@ -0,0 +1,91 @@ +import { StatCard } from "@aidoit/ui"; + +const navigation = [ + "Overview", + "Projects", + "Runs", + "Tasks", + "Teams", + "Tools", + "Deployments", + "Servers", +]; + +export default function HomePage() { + return ( +
+ + +
+
+
+

AIDOIT CONTROL CENTER

+

Good evening, Pavel.

+

+ Build applications, orchestrate AI teams, and manage infrastructure + from one workspace. +

+
+ +
+ +
+ + + + +
+ +
+
+
+
+

WORKSPACE

+

Recent projects

+
+ View all +
+
+
+
+

No projects yet

+

Create the first project and assign it to an AI team.

+
+
+ +
+

ACTIVITY

+

Harness feed

+
+ +
+ Platform initialized +

Professional monorepo foundation is ready.

+
+
+
+
+
+
+ ); +} diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts new file mode 100644 index 0000000..dbf57ed --- /dev/null +++ b/apps/web/next.config.ts @@ -0,0 +1,8 @@ +import type { NextConfig } from "next"; + +const config: NextConfig = { + output: "standalone", + transpilePackages: ["@aidoit/ui", "@aidoit/contracts", "@aidoit/sdk"], +}; + +export default config; diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..2e66545 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,28 @@ +{ + "name": "@aidoit/web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "next start --port 3000", + "lint": "next lint", + "typecheck": "tsc --noEmit", + "test": "echo \"No web tests yet\"" + }, + "dependencies": { + "@aidoit/contracts": "workspace:*", + "@aidoit/sdk": "workspace:*", + "@aidoit/ui": "workspace:*", + "next": "^15.2.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@aidoit/config": "workspace:*", + "@types/node": "^22.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.8.0" + } +} \ No newline at end of file diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..36e23c0 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@aidoit/config/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..e08bcee --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,49 @@ +# Architecture overview + +## Principles + +1. Single user, but authentication is still mandatory. +2. One shared backend for all products. +3. Modular monolith before microservices. +4. AI harness remains independent from UI applications. +5. Tool access is explicitly permissioned per team. +6. Long-running work is executed asynchronously. +7. Generated artifacts are stored outside the relational database. + +## Products + +### Web + +Application development dashboard for projects, prompts, runs, tasks, preview +environments, reviews and deployments. + +### Desktop + +Native Tauri application for local filesystem, shell, Git, Docker, Ollama and +desktop application builds. + +### MSP + +Infrastructure dashboard for VPS, Proxmox, LXC, VM, Docker, firewall, reverse +proxy, monitoring, updates and backups. + +## Core backend modules + +- authentication +- projects +- runs +- tasks +- teams +- tools +- artifacts +- builds +- deployments +- servers +- monitoring +- audit log + +## Data systems + +- PostgreSQL: relational source of truth +- Redis: queues, locks and ephemeral state +- MinIO: generated files, logs and build artifacts diff --git a/docs/decisions/0001-modular-monolith.md b/docs/decisions/0001-modular-monolith.md new file mode 100644 index 0000000..7b563fe --- /dev/null +++ b/docs/decisions/0001-modular-monolith.md @@ -0,0 +1,18 @@ +# ADR 0001: Start as a modular monolith + +## Status + +Accepted + +## Decision + +The first production version uses a single FastAPI deployment with explicit +module boundaries. + +## Reasons + +- one operator +- easier local development +- simpler deployment and backups +- no premature distributed systems complexity +- modules can be extracted later when justified by real load or isolation needs diff --git a/docs/decisions/0002-single-user-security.md b/docs/decisions/0002-single-user-security.md new file mode 100644 index 0000000..7891f01 --- /dev/null +++ b/docs/decisions/0002-single-user-security.md @@ -0,0 +1,18 @@ +# ADR 0002: Single-user security model + +## Status + +Accepted + +## Decision + +Aidoit is single-user software, but no sensitive endpoint is anonymously +accessible in production. + +Initial access: + +- Tailscale for private network access +- bearer token for development +- later PocketID or Authentik for browser login +- separate execution permissions for every AI team +- immutable audit records for tool calls and infrastructure changes diff --git a/docs/roadmap/v0.1.md b/docs/roadmap/v0.1.md new file mode 100644 index 0000000..d50769f --- /dev/null +++ b/docs/roadmap/v0.1.md @@ -0,0 +1,36 @@ +# v0.1 roadmap + +## Foundation + +- monorepo and CI +- shared design system +- shared API contracts and SDK +- PostgreSQL migrations +- authentication +- project CRUD + +## Harness + +- team definitions +- tool registry +- task graph +- run execution +- event streaming +- logs and artifacts + +## Development workflow + +- Gitea integration +- repository checkout +- isolated workspaces +- preview Docker builds +- code review flow +- deployment approvals + +## MSP + +- server inventory +- SSH connection profiles +- Docker and Proxmox adapters +- monitoring integration +- backups and update plans diff --git a/infrastructure/docker/compose.yml b/infrastructure/docker/compose.yml new file mode 100644 index 0000000..23f7fe5 --- /dev/null +++ b/infrastructure/docker/compose.yml @@ -0,0 +1,66 @@ +services: + postgres: + image: postgres:17-alpine + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + minio: + image: minio/minio:latest + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + + api: + build: + context: ../../apps/api + restart: unless-stopped + env_file: + - ../../.env + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + REDIS_URL: redis://redis:6379/0 + MINIO_ENDPOINT: http://minio:9000 + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + +volumes: + postgres_data: + redis_data: + minio_data: diff --git a/infrastructure/nginx/README.md b/infrastructure/nginx/README.md new file mode 100644 index 0000000..1105598 --- /dev/null +++ b/infrastructure/nginx/README.md @@ -0,0 +1,10 @@ +# Reverse proxy + +Production proxy hosts should route: + +- `web.aidoit.top` to the web application +- `msp.aidoit.top` to the MSP application +- `api.aidoit.top` to the API + +For the first deployment, Nginx Proxy Manager can be used. Keep the API and +databases on a private Docker network and expose only the reverse proxy. diff --git a/package.json b/package.json new file mode 100644 index 0000000..5a38d1c --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "aidoit", + "version": "0.1.0", + "private": true, + "packageManager": "pnpm@10.0.0", + "scripts": { + "dev": "turbo dev", + "build": "turbo build", + "lint": "turbo lint", + "typecheck": "turbo typecheck", + "test": "turbo test", + "format": "prettier --write .", + "format:check": "prettier --check .", + "dev:web": "pnpm --filter @aidoit/web dev", + "dev:msp": "pnpm --filter @aidoit/msp dev", + "dev:api": "cd apps/api && python -m uvicorn app.main:app --reload --port 8000" + }, + "devDependencies": { + "prettier": "^3.5.0", + "turbo": "^2.5.0", + "typescript": "^5.8.0" + } +} \ No newline at end of file diff --git a/packages/config/base.json b/packages/config/base.json new file mode 100644 index 0000000..819d0dc --- /dev/null +++ b/packages/config/base.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + } +} diff --git a/packages/config/nextjs.json b/packages/config/nextjs.json new file mode 100644 index 0000000..ab8c01f --- /dev/null +++ b/packages/config/nextjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./base.json", + "compilerOptions": { + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }] + } +} diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000..5e01c96 --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,8 @@ +{ + "name": "@aidoit/config", + "version": "0.1.0", + "private": true, + "files": [ + "*.json" + ] +} \ No newline at end of file diff --git a/packages/config/react-library.json b/packages/config/react-library.json new file mode 100644 index 0000000..16d8701 --- /dev/null +++ b/packages/config/react-library.json @@ -0,0 +1,6 @@ +{ + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx" + } +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 0000000..7892cc9 --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@aidoit/contracts", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "lint": "tsc --noEmit", + "typecheck": "tsc --noEmit", + "test": "echo \"No contract tests yet\"" + }, + "devDependencies": { + "@aidoit/config": "workspace:*", + "typescript": "^5.8.0" + } +} \ No newline at end of file diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts new file mode 100644 index 0000000..1c4e4df --- /dev/null +++ b/packages/contracts/src/index.ts @@ -0,0 +1,15 @@ +export type ProductType = "web" | "desktop" | "msp"; + +export interface Project { + id: number; + name: string; + description: string; + product: ProductType; + created_at: string; +} + +export interface CreateProjectRequest { + name: string; + description?: string; + product?: ProductType; +} diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 0000000..83ab581 --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@aidoit/config/base.json", + "include": ["src"] +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 0000000..89210c6 --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,22 @@ +{ + "name": "@aidoit/sdk", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "tsc --noEmit", + "lint": "tsc --noEmit", + "typecheck": "tsc --noEmit", + "test": "echo \"No SDK tests yet\"" + }, + "dependencies": { + "@aidoit/contracts": "workspace:*" + }, + "devDependencies": { + "@aidoit/config": "workspace:*", + "typescript": "^5.8.0" + } +} \ No newline at end of file diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts new file mode 100644 index 0000000..550960a --- /dev/null +++ b/packages/sdk/src/index.ts @@ -0,0 +1,36 @@ +import type { CreateProjectRequest, Project } from "@aidoit/contracts"; + +export class AidoitClient { + public constructor( + private readonly baseUrl: string, + private readonly token?: string, + ) {} + + private async request(path: string, init?: RequestInit): Promise { + const response = await fetch(`${this.baseUrl}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), + ...init?.headers, + }, + }); + + if (!response.ok) { + throw new Error(`Aidoit API request failed: ${response.status}`); + } + + return response.json() as Promise; + } + + public listProjects(): Promise { + return this.request("/api/v1/projects"); + } + + public createProject(payload: CreateProjectRequest): Promise { + return this.request("/api/v1/projects", { + method: "POST", + body: JSON.stringify(payload), + }); + } +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 0000000..83ab581 --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@aidoit/config/base.json", + "include": ["src"] +} diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..b5ddb95 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,24 @@ +{ + "name": "@aidoit/ui", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./styles.css": "./src/styles.css" + }, + "scripts": { + "build": "tsc --noEmit", + "lint": "tsc --noEmit", + "typecheck": "tsc --noEmit", + "test": "echo \"No UI tests yet\"" + }, + "peerDependencies": { + "react": "^19.0.0" + }, + "devDependencies": { + "@aidoit/config": "workspace:*", + "@types/react": "^19.0.0", + "typescript": "^5.8.0" + } +} \ No newline at end of file diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..74cee37 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1 @@ +export { StatCard } from "./stat-card"; diff --git a/packages/ui/src/stat-card.tsx b/packages/ui/src/stat-card.tsx new file mode 100644 index 0000000..6bc7d2a --- /dev/null +++ b/packages/ui/src/stat-card.tsx @@ -0,0 +1,15 @@ +type StatCardProps = { + label: string; + value: string; + detail: string; +}; + +export function StatCard({ label, value, detail }: StatCardProps) { + return ( +
+ {label} + {value} +

{detail}

+
+ ); +} diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css new file mode 100644 index 0000000..2b28e9b --- /dev/null +++ b/packages/ui/src/styles.css @@ -0,0 +1,52 @@ +:root { + color-scheme: dark; + --background: #071019; + --surface: #0d1824; + --surface-soft: #101e2c; + --border: rgba(255, 255, 255, 0.08); + --text: #eef6ff; + --muted: #8ca0b5; + --accent: #57c7ff; +} + +* { + box-sizing: border-box; +} + +html { + background: var(--background); +} + +body { + margin: 0; + background: + radial-gradient(circle at 80% 0%, rgba(45, 139, 196, 0.12), transparent 28%), + var(--background); + color: var(--text); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +.aidoit-stat-card { + padding: 20px; + border: 1px solid var(--border); + background: linear-gradient(180deg, rgba(16, 30, 44, 0.96), rgba(10, 22, 32, 0.96)); + border-radius: 18px; +} + +.aidoit-stat-card span, +.aidoit-stat-card p { + color: var(--muted); +} + +.aidoit-stat-card strong { + display: block; + font-size: 34px; + margin: 18px 0 5px; +} + +.aidoit-stat-card p { + margin: 0; + font-size: 12px; +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..70c2a8d --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@aidoit/config/react-library.json", + "include": ["src"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..f45d0b7 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - apps/web + - apps/msp + - packages/* diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..049c840 --- /dev/null +++ b/turbo.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "dev": { + "cache": false, + "persistent": true + }, + "build": { + "dependsOn": ["^build"], + "outputs": [".next/**", "dist/**", "! .next/cache/**"] + }, + "lint": { + "dependsOn": ["^lint"] + }, + "typecheck": { + "dependsOn": ["^typecheck"] + }, + "test": { + "dependsOn": ["^build"], + "outputs": ["coverage/**"] + } + } +}