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:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Import design tokens — include this once at your app root */
|
||||
@import "@{{project-slug}}/tokens/css";
|
||||
Reference in New Issue
Block a user