feat: add Turborepo per-project monorepo scaffold and project API

- Add Turborepo scaffold templates (apps: product, website, admin, storybook; packages: ui, tokens, types, config)
- Add ProjectRecord and AppRecord types to control plane
- Add Gitea integration service (repo creation, scaffold push, webhooks)
- Add Coolify integration service (project + per-app service provisioning with turbo --filter)
- Add project routes: GET/POST /projects, GET /projects/:id/apps, POST /projects/:id/deploy
- Update chat route to inject project/monorepo context into AI requests
- Add deploy_app and scaffold_app tools to Gemini tool set
- Update deploy executor with monorepo-aware /execute/deploy endpoint
- Add TURBOREPO_MIGRATION_PLAN.md documenting rationale and scope

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-21 15:07:35 -08:00
parent 57b9ce2f1a
commit 2c3e7f9dfb
40 changed files with 1625 additions and 33 deletions

View File

@@ -0,0 +1,29 @@
{
"name": "@{{project-slug}}/ui",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./styles": "./src/styles.css"
},
"scripts": {
"type-check": "tsc --noEmit",
"lint": "eslint ."
},
"dependencies": {
"@{{project-slug}}/tokens": "workspace:*",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@{{project-slug}}/config": "workspace:*",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}

View File

@@ -0,0 +1,30 @@
import type { HTMLAttributes } from "react";
type BadgeVariant = "default" | "success" | "warning" | "error" | "brand";
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: BadgeVariant;
}
const variantClasses: Record<BadgeVariant, string> = {
default: "bg-[var(--color-neutral-100)] text-[var(--color-neutral-700)]",
success: "bg-[var(--color-success-light)] text-[var(--color-success-dark,#15803d)]",
warning: "bg-[var(--color-warning-light)] text-[var(--color-warning-dark,#b45309)]",
error: "bg-[var(--color-error-light)] text-[var(--color-error-dark,#b91c1c)]",
brand: "bg-[var(--color-brand-100)] text-[var(--color-brand-700)]",
};
export function Badge({ variant = "default", className = "", children, ...props }: BadgeProps) {
return (
<span
{...props}
className={[
"inline-flex items-center rounded-[var(--radius-full)] px-2.5 py-0.5 text-xs font-medium",
variantClasses[variant],
className,
].join(" ")}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,56 @@
import type { ButtonHTMLAttributes } from "react";
type Variant = "primary" | "secondary" | "ghost" | "destructive";
type Size = "sm" | "md" | "lg";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
loading?: boolean;
}
const variantClasses: Record<Variant, string> = {
primary: "bg-[var(--color-brand-600)] text-white hover:bg-[var(--color-brand-700)]",
secondary: "bg-[var(--color-neutral-100)] text-[var(--color-neutral-900)] hover:bg-[var(--color-neutral-200)]",
ghost: "bg-transparent text-[var(--color-neutral-700)] hover:bg-[var(--color-neutral-100)]",
destructive: "bg-[var(--color-error)] text-white hover:opacity-90",
};
const sizeClasses: Record<Size, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-sm",
lg: "px-5 py-2.5 text-base",
};
export function Button({
variant = "primary",
size = "md",
loading = false,
disabled,
className = "",
children,
...props
}: ButtonProps) {
return (
<button
{...props}
disabled={disabled ?? loading}
className={[
"inline-flex items-center justify-center gap-2 rounded-[var(--radius-md)] font-medium transition-colors",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
"disabled:opacity-50 disabled:cursor-not-allowed",
variantClasses[variant],
sizeClasses[size],
className,
].join(" ")}
>
{loading && (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" />
</svg>
)}
{children}
</button>
);
}

View File

@@ -0,0 +1,27 @@
import type { HTMLAttributes } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
padding?: "none" | "sm" | "md" | "lg";
}
const paddingClasses = {
none: "",
sm: "p-3",
md: "p-5",
lg: "p-8",
};
export function Card({ padding = "md", className = "", children, ...props }: CardProps) {
return (
<div
{...props}
className={[
"rounded-[var(--radius-xl)] border border-[var(--color-neutral-200)] bg-white shadow-[var(--shadow-sm,0_1px_2px_0_rgb(0_0_0/0.05))]",
paddingClasses[padding],
className,
].join(" ")}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,37 @@
import type { InputHTMLAttributes } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
hint?: string;
}
export function Input({ label, error, hint, className = "", id, ...props }: InputProps) {
const inputId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
return (
<div className="flex flex-col gap-1">
{label && (
<label htmlFor={inputId} className="text-sm font-medium text-[var(--color-neutral-700)]">
{label}
</label>
)}
<input
{...props}
id={inputId}
className={[
"w-full rounded-[var(--radius-md)] border px-3 py-2 text-sm transition-colors",
"placeholder:text-[var(--color-neutral-400)]",
"focus:outline-none focus:ring-2 focus:ring-[var(--color-brand-500)] focus:border-transparent",
"disabled:opacity-50 disabled:cursor-not-allowed",
error
? "border-[var(--color-error)] bg-[var(--color-error-light)]"
: "border-[var(--color-neutral-300)] bg-white",
className,
].join(" ")}
/>
{error && <p className="text-xs text-[var(--color-error)]">{error}</p>}
{hint && !error && <p className="text-xs text-[var(--color-neutral-500)]">{hint}</p>}
</div>
);
}

View File

@@ -0,0 +1,4 @@
export { Button } from "./components/Button.js";
export { Card } from "./components/Card.js";
export { Input } from "./components/Input.js";
export { Badge } from "./components/Badge.js";

View File

@@ -0,0 +1,2 @@
/* Import design tokens — include this once at your app root */
@import "@{{project-slug}}/tokens/css";