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,23 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
/** @type {import("typescript-eslint").Config} */
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
plugins: {
react: reactPlugin,
"react-hooks": reactHooksPlugin,
},
rules: {
"react/react-in-jsx-scope": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/consistent-type-imports": "error",
},
}
);

View File

@@ -0,0 +1,9 @@
{
"name": "@{{project-slug}}/config",
"version": "0.1.0",
"private": true,
"exports": {
"./tsconfig.base.json": "./tsconfig.base.json",
"./eslint": "./eslint.config.js"
}
}

View File

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

View File

@@ -0,0 +1,84 @@
export const colors = {
brand: {
50: "#f0f9ff",
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9",
600: "#0284c7",
700: "#0369a1",
800: "#075985",
900: "#0c4a6e",
},
neutral: {
50: "#fafafa",
100: "#f4f4f5",
200: "#e4e4e7",
300: "#d4d4d8",
400: "#a1a1aa",
500: "#71717a",
600: "#52525b",
700: "#3f3f46",
800: "#27272a",
900: "#18181b",
},
success: { DEFAULT: "#22c55e", light: "#dcfce7", dark: "#15803d" },
warning: { DEFAULT: "#f59e0b", light: "#fef3c7", dark: "#b45309" },
error: { DEFAULT: "#ef4444", light: "#fee2e2", dark: "#b91c1c" },
} as const;
export const typography = {
fontFamily: {
sans: "var(--font-sans, ui-sans-serif, system-ui, sans-serif)",
mono: "var(--font-mono, ui-monospace, monospace)",
},
fontSize: {
xs: ["0.75rem", { lineHeight: "1rem" }],
sm: ["0.875rem", { lineHeight: "1.25rem" }],
base: ["1rem", { lineHeight: "1.5rem" }],
lg: ["1.125rem", { lineHeight: "1.75rem" }],
xl: ["1.25rem", { lineHeight: "1.75rem" }],
"2xl":["1.5rem", { lineHeight: "2rem" }],
"3xl":["1.875rem", { lineHeight: "2.25rem" }],
"4xl":["2.25rem", { lineHeight: "2.5rem" }],
},
} as const;
export const spacing = {
px: "1px",
0: "0",
1: "0.25rem",
2: "0.5rem",
3: "0.75rem",
4: "1rem",
5: "1.25rem",
6: "1.5rem",
8: "2rem",
10: "2.5rem",
12: "3rem",
16: "4rem",
20: "5rem",
24: "6rem",
32: "8rem",
} as const;
export const radius = {
none: "0",
sm: "0.125rem",
DEFAULT: "0.25rem",
md: "0.375rem",
lg: "0.5rem",
xl: "0.75rem",
"2xl":"1rem",
full: "9999px",
} as const;
export const shadows = {
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
DEFAULT: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
none:"none",
} as const;

View File

@@ -0,0 +1,46 @@
:root {
/* Brand */
--color-brand-50: #f0f9ff;
--color-brand-100: #e0f2fe;
--color-brand-200: #bae6fd;
--color-brand-300: #7dd3fc;
--color-brand-400: #38bdf8;
--color-brand-500: #0ea5e9;
--color-brand-600: #0284c7;
--color-brand-700: #0369a1;
--color-brand-800: #075985;
--color-brand-900: #0c4a6e;
/* Neutral */
--color-neutral-50: #fafafa;
--color-neutral-100: #f4f4f5;
--color-neutral-200: #e4e4e7;
--color-neutral-300: #d4d4d8;
--color-neutral-400: #a1a1aa;
--color-neutral-500: #71717a;
--color-neutral-600: #52525b;
--color-neutral-700: #3f3f46;
--color-neutral-800: #27272a;
--color-neutral-900: #18181b;
/* Semantic */
--color-success: #22c55e;
--color-success-light: #dcfce7;
--color-warning: #f59e0b;
--color-warning-light: #fef3c7;
--color-error: #ef4444;
--color-error-light: #fee2e2;
/* Typography */
--font-sans: ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, monospace;
/* Radius */
--radius-sm: 0.125rem;
--radius: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-full: 9999px;
}

View File

@@ -0,0 +1,17 @@
{
"name": "@{{project-slug}}/types",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"type-check": "tsc --noEmit",
"lint": "eslint ."
},
"devDependencies": {
"@{{project-slug}}/config": "workspace:*",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,35 @@
/**
* Shared types for {{project-name}}
*
* Add types here that are used across product, website, and admin.
* Import in any app: import type { User } from "@{{project-slug}}/types"
*/
export type ID = string;
export type User = {
id: ID;
email: string;
name: string;
avatarUrl?: string;
createdAt: string;
};
export type ApiResponse<T> = {
data: T;
error: null;
} | {
data: null;
error: {
message: string;
code?: string;
};
};
export type PaginatedResponse<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
};

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";