├── public
├── dashboard-dark.png
└── dashboard-light.png
├── .github
└── funding.yaml
├── .husky
└── pre-commit
├── lib
├── constants
│ ├── linear-config.ts
│ └── buildware-config.ts
├── utils
│ └── index.ts
└── ai
│ ├── estimate-claude-tokens.ts
│ ├── limit-tokens.ts
│ ├── calculate-llm-cost.ts
│ ├── parse-ai-response.ts
│ └── build-plan-prompt.ts
├── postcss.config.mjs
├── types
├── globals.d.ts
├── ai
│ └── index.ts
├── github
│ └── index.ts
└── linear
│ └── linear.ts
├── app
├── [workspaceId]
│ ├── [projectId]
│ │ ├── issues
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ ├── [issueId]
│ │ │ │ ├── edit
│ │ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ │ └── create
│ │ │ │ └── page.tsx
│ │ ├── settings
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── templates
│ │ │ ├── loading.tsx
│ │ │ ├── [id]
│ │ │ │ ├── page.tsx
│ │ │ │ └── edit
│ │ │ │ │ └── page.tsx
│ │ │ ├── create
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── instructions
│ │ │ ├── loading.tsx
│ │ │ ├── page.tsx
│ │ │ ├── create
│ │ │ │ └── page.tsx
│ │ │ └── [id]
│ │ │ │ ├── page.tsx
│ │ │ │ └── edit
│ │ │ │ └── page.tsx
│ │ ├── integrations
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── page.tsx
│ ├── edit
│ │ └── page.tsx
│ └── layout.tsx
├── (auth)
│ ├── login
│ │ └── [[...login]]
│ │ │ └── page.tsx
│ ├── signup
│ │ └── [[...signup]]
│ │ │ └── page.tsx
│ ├── layout.tsx
│ └── onboarding
│ │ └── page.tsx
├── (marketing)
│ ├── layout.tsx
│ └── page.tsx
├── api
│ ├── auth
│ │ └── callback
│ │ │ ├── github
│ │ │ └── route.ts
│ │ │ └── linear
│ │ │ └── route.ts
│ └── linear
│ │ └── webhook
│ │ └── route.ts
├── layout.tsx
├── workspaces
│ └── page.tsx
└── globals.css
├── components
├── ui
│ ├── aspect-ratio.tsx
│ ├── skeleton.tsx
│ ├── collapsible.tsx
│ ├── textarea.tsx
│ ├── label.tsx
│ ├── input.tsx
│ ├── separator.tsx
│ ├── progress.tsx
│ ├── toaster.tsx
│ ├── sonner.tsx
│ ├── checkbox.tsx
│ ├── slider.tsx
│ ├── switch.tsx
│ ├── badge.tsx
│ ├── tooltip.tsx
│ ├── hover-card.tsx
│ ├── popover.tsx
│ ├── avatar.tsx
│ ├── toggle.tsx
│ ├── radio-group.tsx
│ ├── alert.tsx
│ ├── scroll-area.tsx
│ ├── resizable.tsx
│ ├── toggle-group.tsx
│ ├── tabs.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── accordion.tsx
│ ├── input-otp.tsx
│ ├── calendar.tsx
│ └── multi-select.tsx
├── instructions
│ ├── message-markdown-memoized.tsx
│ ├── instruction.tsx
│ ├── use-copy-to-clipboard.tsx
│ ├── new-instruction-form.tsx
│ ├── edit-instruction-form.tsx
│ ├── instruction-list.tsx
│ └── message-markdown.tsx
├── utility
│ ├── theme-provider.tsx
│ ├── not-found.tsx
│ ├── loading-page.tsx
│ └── theme-switcher.tsx
├── issues
│ ├── issue-creation.tsx
│ └── issues-list.tsx
├── dashboard
│ └── reusable
│ │ ├── edit-form.tsx
│ │ ├── crud-page.tsx
│ │ ├── data-list.tsx
│ │ ├── data-item.tsx
│ │ └── crud-form.tsx
├── profiles
│ └── profile-creator.tsx
├── workspaces
│ ├── edit-workspace-button.tsx
│ └── create-workspace-button.tsx
├── templates
│ ├── template.tsx
│ ├── handle-save.tsx
│ ├── template-select.tsx
│ ├── template-list.tsx
│ ├── edit-template-form.tsx
│ └── new-template-form.tsx
├── integrations
│ ├── integration.tsx
│ ├── integrations.tsx
│ ├── connect-github.tsx
│ └── connect-linear.tsx
├── projects
│ ├── create-project-button.tsx
│ └── delete-project-button.tsx
├── marketing
│ ├── main-section.tsx
│ └── site-footer.tsx
└── magicui
│ ├── border-beam.tsx
│ └── shine-border.tsx
├── db
├── migrations
│ └── meta
│ │ └── _journal.json
├── schema
│ ├── index.ts
│ ├── profiles-schema.ts
│ ├── workspaces-schema.ts
│ ├── issue-messages-schema.ts
│ ├── embedded-branches-schema.ts
│ ├── templates-schema.ts
│ ├── issues-to-instructions-schema.ts
│ ├── templates-to-instructions-schema.ts
│ ├── issues-schema.ts
│ ├── instructions-schema.ts
│ ├── projects-schema.ts
│ └── embedded-files-schema.ts
├── queries
│ ├── index.ts
│ ├── embedded-files-queries.ts
│ ├── profiles-queries.ts
│ ├── issues-queries.ts
│ ├── issues-to-instructions-queries.ts
│ ├── templates-to-instructions-queries.ts
│ ├── instructions-queries.ts
│ ├── workspaces-queries.ts
│ ├── embedded-branches-queries.ts
│ ├── issue-messages-queries.ts
│ └── templates-queries.ts
└── db.ts
├── drizzle.config.ts
├── scripts
└── webhook-proxy
│ └── proxy.ts
├── actions
├── auth
│ └── auth.ts
├── linear
│ ├── reactions.ts
│ ├── webhook.ts
│ └── issues.ts
├── github
│ ├── tokenize-files.ts
│ ├── list-branches.ts
│ ├── auth.ts
│ ├── delete-pr.ts
│ ├── embed-files.ts
│ ├── list-repos.ts
│ ├── embed-branch.ts
│ ├── embed-target-branch.ts
│ ├── fetch-files.ts
│ └── fetch-codebase.ts
├── ai
│ ├── generate-embedding.ts
│ └── generate-ai-response.ts
└── retrieval
│ └── get-similar-files.ts
├── next.config.mjs
├── components.json
├── .gitignore
├── tsconfig.json
├── .env.example
├── prettier.config.cjs
├── middleware.ts
├── license
└── .eslintrc.json
/public/dashboard-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-buildware-ai/main/public/dashboard-dark.png
--------------------------------------------------------------------------------
/public/dashboard-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code/app-buildware-ai/main/public/dashboard-light.png
--------------------------------------------------------------------------------
/.github/funding.yaml:
--------------------------------------------------------------------------------
1 | # If you find my open-source work helpful, please consider sponsoring me!
2 |
3 | github: mckaywrigley
4 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | . "$(dirname -- "$0")/_/husky.sh"
4 |
5 | npm run lint:fix && npm run format:write && git add .
6 |
--------------------------------------------------------------------------------
/lib/constants/linear-config.ts:
--------------------------------------------------------------------------------
1 | export const IN_PROGRESS_EMOJI = "thought_balloon"
2 | export const COMPLETED_EMOJI = "white_check_mark"
3 |
4 | export const AI_LABEL = "Assign To AI"
5 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/types/globals.d.ts:
--------------------------------------------------------------------------------
1 | export {}
2 |
3 | declare global {
4 | interface CustomJwtSessionClaims {
5 | metadata: {
6 | onboardingComplete?: boolean
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/issues/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingPage } from "@/components/utility/loading-page"
2 |
3 | export default async function IssuesLoadingPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingPage } from "@/components/utility/loading-page"
2 |
3 | export default async function SettingsLoadingPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/templates/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingPage } from "@/components/utility/loading-page"
2 |
3 | export default async function TemplatesLoadingPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/instructions/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingPage } from "@/components/utility/loading-page"
2 |
3 | export default async function InstructionsLoadingPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/integrations/loading.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingPage } from "@/components/utility/loading-page"
2 |
3 | export default async function IntegrationsLoadingPage() {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/app/(auth)/login/[[...login]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs"
2 |
3 | export default function LoginPage() {
4 | if (process.env.NEXT_PUBLIC_APP_MODE === "simple") {
5 | return null
6 | }
7 |
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/app/(auth)/signup/[[...signup]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs"
2 |
3 | export default function SignUpPage() {
4 | if (process.env.NEXT_PUBLIC_APP_MODE === "simple") {
5 | return null
6 | }
7 |
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/lib/ai/estimate-claude-tokens.ts:
--------------------------------------------------------------------------------
1 | import { encode } from "gpt-tokenizer"
2 |
3 | export function estimateClaudeSonnet3_5TokenCount(text: string): number {
4 | // Claude tokenizer is ~1.25-1.35 times more expensive than GPT, using 1.4 to be safe
5 | return encode(text).length * 1.4
6 | }
7 |
--------------------------------------------------------------------------------
/db/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7",
3 | "dialect": "postgresql",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "7",
8 | "when": 1721203602599,
9 | "tag": "0000_burly_ben_urich",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/types/ai/index.ts:
--------------------------------------------------------------------------------
1 | export interface AIFileInfo {
2 | path: string
3 | content: string
4 | language: string
5 | status: "new" | "modified" | "deleted"
6 | }
7 |
8 | export interface AIParsedResponse {
9 | fileList: string[]
10 | files: AIFileInfo[]
11 | prTitle: string
12 | }
13 |
--------------------------------------------------------------------------------
/components/instructions/message-markdown-memoized.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from "react";
2 | import ReactMarkdown, { Options } from "react-markdown";
3 |
4 | export const MessageMarkdownMemoized: FC = memo(ReactMarkdown, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.className === nextProps.className);
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | interface AuthLayoutProps {
2 | children: React.ReactNode
3 | }
4 |
5 | export default async function AuthLayout({ children }: AuthLayoutProps) {
6 | return (
7 | <>
8 |
9 | {children}
10 |
11 | >
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/utility/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import { ThemeProviderProps } from "next-themes/dist/types";
5 |
6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7 | return {children};
8 | }
9 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { config } from "dotenv"
2 | import { defineConfig } from "drizzle-kit"
3 |
4 | config({ path: ".env.local" })
5 |
6 | export default defineConfig({
7 | dialect: "postgresql",
8 | dbCredentials: {
9 | url: process.env.DATABASE_URL!
10 | },
11 | schema: "./db/schema/index.ts",
12 | out: "./db/migrations"
13 | })
14 |
--------------------------------------------------------------------------------
/components/utility/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | interface NotFoundProps {
4 | message: string
5 | }
6 |
7 | export const NotFound: FC = ({ message }) => {
8 | return (
9 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/components/issues/issue-creation.tsx:
--------------------------------------------------------------------------------
1 | import { getTemplatesWithInstructionsByProjectId } from "@/db/queries/templates-queries"
2 | import { NewIssueForm } from "./new-issue-form"
3 |
4 | export const IssueCreation = async ({ projectId }: { projectId: string }) => {
5 | const templates = await getTemplatesWithInstructionsByProjectId(projectId)
6 |
7 | return
8 | }
9 |
--------------------------------------------------------------------------------
/lib/constants/buildware-config.ts:
--------------------------------------------------------------------------------
1 | // text-embedding-3-small or text-embedding-3-large
2 | export const BUILDWARE_EMBEDDING_MODEL = "text-embedding-3-large"
3 |
4 | // Between 256 and 3072
5 | export const BUILDWARE_EMBEDDING_DIMENSIONS = 256
6 |
7 | // Max: 8192
8 | export const BUILDWARE_MAX_OUTPUT_TOKENS = 8192
9 |
10 | // Max: 200000
11 | export const BUILDWARE_MAX_INPUT_TOKENS = 200000
12 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/dashboard/reusable/edit-form.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react"
2 |
3 | interface EditFormProps {
4 | action: (formData: FormData) => Promise
5 | children: React.ReactNode
6 | }
7 |
8 | export const EditForm: FC = ({ action, children }) => {
9 | return (
10 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/scripts/webhook-proxy/proxy.ts:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv"
2 | import SmeeClient from "smee-client"
3 |
4 | dotenv.config({ path: ".env.local" })
5 |
6 | const port = process.env.PORT || 3000
7 |
8 | const smee = new SmeeClient({
9 | source: process.env.WEBHOOK_URL!,
10 | target: `http://localhost:${port}/api/linear/webhook`, // LINEAR TEST
11 | logger: console
12 | })
13 |
14 | const events = smee.start()
15 | //
16 |
--------------------------------------------------------------------------------
/components/utility/loading-page.tsx:
--------------------------------------------------------------------------------
1 | import { Loader2 } from "lucide-react"
2 | import { FC } from "react"
3 |
4 | interface LoadingPageProps {
5 | size?: number
6 | }
7 |
8 | export const LoadingPage: FC = ({ size = 16 }) => {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/issues/page.tsx:
--------------------------------------------------------------------------------
1 | import { IssuesList } from "@/components/issues/issues-list"
2 | import { getIssuesByProjectId } from "@/db/queries/issues-queries"
3 |
4 | export const revalidate = 0
5 |
6 | export default async function IssuesPage({
7 | params
8 | }: {
9 | params: { projectId: string }
10 | }) {
11 | const issues = await getIssuesByProjectId(params.projectId)
12 |
13 | return
14 | }
15 |
--------------------------------------------------------------------------------
/actions/auth/auth.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { auth } from "@clerk/nextjs/server"
4 |
5 | const IS_SIMPLE_MODE = process.env.NEXT_PUBLIC_APP_MODE === "simple"
6 | const SIMPLE_USER_ID = "simple_user_1"
7 |
8 | export async function getUserId() {
9 | if (IS_SIMPLE_MODE) {
10 | return SIMPLE_USER_ID
11 | }
12 |
13 | const { userId } = auth()
14 |
15 | if (!userId) {
16 | throw new Error("User not authenticated")
17 | }
18 |
19 | return userId
20 | }
21 |
--------------------------------------------------------------------------------
/actions/linear/reactions.ts:
--------------------------------------------------------------------------------
1 | import { LinearClient } from "@linear/sdk"
2 |
3 | export const createReaction = async (
4 | linearClient: LinearClient,
5 | id: string,
6 | emoji: string,
7 | isComment = false
8 | ) => {
9 | try {
10 | return await linearClient.createReaction({
11 | emoji,
12 | ...(isComment ? { commentId: id } : { issueId: id })
13 | })
14 | } catch (error) {
15 | console.error("Error creating reaction:", error)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: false,
4 | images: {
5 | remotePatterns: [
6 | {
7 | protocol: "http",
8 | hostname: "localhost"
9 | },
10 | {
11 | protocol: "http",
12 | hostname: "127.0.0.1"
13 | },
14 | {
15 | protocol: "https",
16 | hostname: "**"
17 | }
18 | ]
19 | }
20 | }
21 |
22 | export default nextConfig
23 |
--------------------------------------------------------------------------------
/db/schema/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./embedded-branches-schema"
2 | export * from "./embedded-files-schema"
3 | export * from "./instructions-schema"
4 | export * from "./issue-messages-schema"
5 | export * from "./issues-schema"
6 | export * from "./issues-to-instructions-schema"
7 | export * from "./profiles-schema"
8 | export * from "./projects-schema"
9 | export * from "./templates-schema"
10 | export * from "./templates-to-instructions-schema"
11 | export * from "./workspaces-schema"
12 |
--------------------------------------------------------------------------------
/app/[workspaceId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getWorkspaceById } from "@/db/queries/workspaces-queries"
2 |
3 | export const revalidate = 0
4 |
5 | export default async function WorkspacePage({
6 | params
7 | }: {
8 | params: { workspaceId: string }
9 | }) {
10 | const { workspaceId } = params
11 |
12 | const workspaces = await getWorkspaceById(workspaceId)
13 |
14 | if (!workspaces) {
15 | return Workspace not found
16 | }
17 |
18 | return {workspaces.name}
19 | }
20 |
--------------------------------------------------------------------------------
/db/queries/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./embedded-branches-queries"
2 | export * from "./embedded-files-queries"
3 | export * from "./instructions-queries"
4 | export * from "./issue-messages-queries"
5 | export * from "./issues-queries"
6 | export * from "./issues-to-instructions-queries"
7 | export * from "./profiles-queries"
8 | export * from "./projects-queries"
9 | export * from "./templates-queries"
10 | export * from "./templates-to-instructions-queries"
11 | export * from "./workspaces-queries"
12 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "magicui": "@/components/magicui"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { SiteFooter } from "@/components/marketing/site-footer"
2 | import { SiteHeader } from "@/components/marketing/site-header"
3 |
4 | interface MarketingLayoutProps {
5 | children: React.ReactNode
6 | }
7 |
8 | export default async function MarketingLayout({
9 | children
10 | }: MarketingLayoutProps) {
11 | return (
12 | <>
13 |
14 | {children}
15 |
16 | >
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/instructions/page.tsx:
--------------------------------------------------------------------------------
1 | import { InstructionsList } from "@/components/instructions/instruction-list"
2 | import { getInstructionsByProjectId } from "@/db/queries/instructions-queries"
3 |
4 | export const revalidate = 0
5 |
6 | export default async function InstructionPage({
7 | params
8 | }: {
9 | params: { projectId: string }
10 | }) {
11 | const instructions = await getInstructionsByProjectId(params.projectId)
12 |
13 | return
14 | }
15 |
--------------------------------------------------------------------------------
/db/schema/profiles-schema.ts:
--------------------------------------------------------------------------------
1 | import { pgTable, text, timestamp } from "drizzle-orm/pg-core"
2 |
3 | export const profilesTable = pgTable("profiles", {
4 | userId: text("user_id").primaryKey(),
5 | createdAt: timestamp("created_at").notNull().defaultNow(),
6 | updatedAt: timestamp("updated_at")
7 | .notNull()
8 | .defaultNow()
9 | .$onUpdate(() => new Date())
10 | })
11 |
12 | export type InsertProfile = typeof profilesTable.$inferInsert
13 | export type SelectProfile = typeof profilesTable.$inferSelect
14 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/issues/[issueId]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import { EditIssueForm } from "@/components/issues/edit-issue-form"
2 | import { getIssueById } from "@/db/queries/issues-queries"
3 |
4 | export const revalidate = 0
5 |
6 | export default async function EditIssuePage({
7 | params
8 | }: {
9 | params: { issueId: string }
10 | }) {
11 | const issue = await getIssueById(params.issueId)
12 |
13 | if (!issue) {
14 | return Issue not found
15 | }
16 |
17 | return
18 | }
19 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/instructions/create/page.tsx:
--------------------------------------------------------------------------------
1 | import { CRUDPage } from "@/components/dashboard/reusable/crud-page"
2 | import NewInstructionForm from "@/components/instructions/new-instruction-form"
3 |
4 | export const revalidate = 0
5 |
6 | export default async function CreateInstructionPage() {
7 | return (
8 |
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/actions/github/tokenize-files.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { GitHubFileContent } from "@/types/github"
4 | import { encode } from "gpt-tokenizer"
5 |
6 | export async function tokenizeFiles(filesContent: GitHubFileContent[]) {
7 | // Filter out files with more than 8k tokens and 0 tokens
8 | const tokenizedFiles = filesContent.filter(
9 | file =>
10 | typeof file.content === "string" &&
11 | encode(file.content).length <= 8000 &&
12 | encode(file.content).length > 0
13 | )
14 |
15 | return tokenizedFiles
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/issues/create/page.tsx:
--------------------------------------------------------------------------------
1 | import { CRUDPage } from "@/components/dashboard/reusable/crud-page"
2 | import { IssueCreation } from "@/components/issues/issue-creation"
3 |
4 | export const revalidate = 0
5 |
6 | export default async function CreateIssuePage({
7 | params
8 | }: {
9 | params: { projectId: string }
10 | }) {
11 | return (
12 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getProjectById } from "@/db/queries/projects-queries"
2 |
3 | export const revalidate = 0
4 |
5 | export default async function ProjectPage({
6 | params
7 | }: {
8 | params: { projectId: string; workspaceId: string }
9 | }) {
10 | const project = await getProjectById(params.projectId)
11 |
12 | if (!project) {
13 | return Project not found
14 | }
15 |
16 | return (
17 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/actions/github/list-branches.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getAuthenticatedOctokit } from "./auth"
4 |
5 | export const listBranches = async (
6 | installationId: number | null,
7 | repoFullName: string
8 | ): Promise => {
9 | try {
10 | const octokit = await getAuthenticatedOctokit(installationId)
11 |
12 | const [owner, repo] = repoFullName.split("/")
13 | const { data } = await octokit.repos.listBranches({ owner, repo })
14 |
15 | return data.map(branch => branch.name)
16 | } catch (error: any) {
17 | console.error("Error fetching branches:", error)
18 | return []
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/components/instructions/instruction.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { deleteInstruction } from "@/db/queries/instructions-queries"
4 | import { SelectInstruction } from "@/db/schema"
5 | import { FC } from "react"
6 | import { InstructionTemplateView } from "../dashboard/reusable/instruction-template-view"
7 |
8 | interface InstructionsProps {
9 | instruction: SelectInstruction
10 | }
11 |
12 | export const Instruction: FC = ({ instruction }) => {
13 | return (
14 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/templates/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Template } from "@/components/templates/template"
2 | import { NotFound } from "@/components/utility/not-found"
3 | import { getTemplateWithInstructionById } from "@/db/queries/templates-queries"
4 |
5 | export const revalidate = 0
6 |
7 | export default async function TemplatePage({
8 | params
9 | }: {
10 | params: { id: string; projectId: string }
11 | }) {
12 | const template = await getTemplateWithInstructionById(params.id)
13 |
14 | if (!template) {
15 | return
16 | }
17 |
18 | return
19 | }
20 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/instructions/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { Instruction } from "@/components/instructions/instruction"
2 | import { NotFound } from "@/components/utility/not-found"
3 | import { getInstructionById } from "@/db/queries/instructions-queries"
4 |
5 | export const revalidate = 0
6 |
7 | export default async function InstructionPage({
8 | params
9 | }: {
10 | params: { id: string; projectId: string }
11 | }) {
12 | const instruction = await getInstructionById(params.id)
13 |
14 | if (!instruction) {
15 | return
16 | }
17 |
18 | return
19 | }
20 |
--------------------------------------------------------------------------------
/actions/ai/generate-embedding.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import {
4 | BUILDWARE_EMBEDDING_DIMENSIONS,
5 | BUILDWARE_EMBEDDING_MODEL
6 | } from "@/lib/constants/buildware-config"
7 | import OpenAI from "openai"
8 |
9 | const openai = new OpenAI()
10 |
11 | export async function generateEmbedding(text: string) {
12 | try {
13 | const response = await openai.embeddings.create({
14 | model: BUILDWARE_EMBEDDING_MODEL,
15 | dimensions: BUILDWARE_EMBEDDING_DIMENSIONS,
16 | input: text
17 | })
18 |
19 | return response.data[0].embedding
20 | } catch (error) {
21 | console.error("Error generating embedding:", error)
22 | throw error
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/[workspaceId]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import { NotFound } from "@/components/utility/not-found"
2 | import { EditWorkspaceClient } from "@/components/workspaces/edit-workspace-client"
3 | import { getWorkspaceById } from "@/db/queries/workspaces-queries"
4 |
5 | export default async function EditWorkspace({
6 | params
7 | }: {
8 | params: { workspaceId: string }
9 | }) {
10 | const workspace = await getWorkspaceById(params.workspaceId)
11 |
12 | if (!workspace) {
13 | return
14 | }
15 |
16 | return (
17 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "downlevelIteration": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/app/(marketing)/page.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatedGridPattern } from "@/components/magicui/animated-grid-pattern"
2 | import MainSection from "@/components/marketing/main-section"
3 | import { cn } from "@/lib/utils"
4 |
5 | export default async function MarketingPage() {
6 | return (
7 | <>
8 |
17 |
18 |
19 |
20 |
21 | >
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/templates/create/page.tsx:
--------------------------------------------------------------------------------
1 | import { CRUDPage } from "@/components/dashboard/reusable/crud-page"
2 | import NewTemplateForm from "@/components/templates/new-template-form"
3 | import { getInstructionsByProjectId } from "@/db/queries/instructions-queries"
4 |
5 | export const revalidate = 0
6 |
7 | export default async function CreateTemplatePage({
8 | params
9 | }: {
10 | params: { projectId: string }
11 | }) {
12 | const instructions = await getInstructionsByProjectId(params.projectId)
13 |
14 | return (
15 |
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/types/github/index.ts:
--------------------------------------------------------------------------------
1 | export interface GitHubRepository {
2 | id: number
3 | full_name: string
4 | name: string
5 | private: boolean
6 | html_url: string
7 | description: string | null
8 | }
9 |
10 | export interface GitHubFile {
11 | type: "file" | "dir" | "submodule" | "symlink"
12 | size: number
13 | name: string
14 | path: string
15 | content?: string
16 | sha: string
17 | url: string
18 | git_url: string | null
19 | html_url: string | null
20 | download_url: string | null
21 | _links: {
22 | self: string
23 | git: string | null
24 | html: string | null
25 | }
26 | owner: string
27 | repo: string
28 | ref: string
29 | }
30 |
31 | export interface GitHubFileContent {
32 | path: string
33 | name: string
34 | content: string
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/instructions/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | export interface useCopyToClipboardProps {
6 | timeout?: number;
7 | }
8 |
9 | export function useCopyToClipboard({ timeout = 2000 }: useCopyToClipboardProps) {
10 | const [isCopied, setIsCopied] = useState(false);
11 |
12 | const copyToClipboard = (value: string) => {
13 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
14 | return;
15 | }
16 |
17 | if (!value) {
18 | return;
19 | }
20 |
21 | navigator.clipboard.writeText(value).then(() => {
22 | setIsCopied(true);
23 |
24 | setTimeout(() => {
25 | setIsCopied(false);
26 | }, timeout);
27 | });
28 | };
29 |
30 | return { isCopied, copyToClipboard };
31 | }
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/utility/theme-switcher.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Moon, Sun } from "lucide-react"
4 | import { useTheme } from "next-themes"
5 | import { FC } from "react"
6 | import { Button } from "../ui/button"
7 |
8 | interface ThemeSwitcherProps {}
9 |
10 | export const ThemeSwitcher: FC = () => {
11 | const { setTheme, theme } = useTheme()
12 |
13 | const handleChange = (theme: "dark" | "light") => {
14 | localStorage.setItem("theme", theme)
15 | setTheme(theme)
16 | }
17 |
18 | return (
19 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/instructions/[id]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import { CRUDPage } from "@/components/dashboard/reusable/crud-page"
2 | import EditInstructionForm from "@/components/instructions/edit-instruction-form"
3 | import { NotFound } from "@/components/utility/not-found"
4 | import { getInstructionById } from "@/db/queries/instructions-queries"
5 |
6 | export const revalidate = 0
7 |
8 | export default async function EditInstructionPage({
9 | params
10 | }: {
11 | params: { id: string }
12 | }) {
13 | const instruction = await getInstructionById(params.id)
14 |
15 | if (!instruction) {
16 | return
17 | }
18 |
19 | return (
20 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as ProgressPrimitive from "@radix-ui/react-progress"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { listRepos } from "@/actions/github/list-repos"
2 | import { ProjectSetup } from "@/components/projects/project-setup"
3 | import { getProjectById } from "@/db/queries/projects-queries"
4 | import { GitHubRepository } from "@/types/github"
5 |
6 | export const revalidate = 0
7 |
8 | export default async function SettingsPage({
9 | params
10 | }: {
11 | params: { projectId: string; workspaceId: string }
12 | }) {
13 | const project = await getProjectById(params.projectId)
14 |
15 | if (!project) {
16 | return Project not found
17 | }
18 |
19 | let repos: GitHubRepository[] = []
20 | repos = await listRepos(project.githubInstallationId)
21 |
22 | return (
23 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/app/(auth)/onboarding/page.tsx:
--------------------------------------------------------------------------------
1 | import { ProfileCreator } from "@/components/profiles/profile-creator"
2 | import { getProfileByUserId } from "@/db/queries/profiles-queries"
3 | import { Loader2 } from "lucide-react"
4 | import { redirect } from "next/navigation"
5 | import { Suspense } from "react"
6 |
7 | export const revalidate = 0
8 |
9 | export default async function OnboardingPage() {
10 | const profile = await getProfileByUserId()
11 |
12 | if (!profile) {
13 | return (
14 |
17 |
18 | Creating profile
19 |
20 | }
21 | >
22 |
23 |
24 | )
25 | }
26 |
27 | if (profile) {
28 | return redirect("/workspaces")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/actions/github/auth.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { createAppAuth } from "@octokit/auth-app"
4 | import { Octokit } from "@octokit/rest"
5 |
6 | export async function getAuthenticatedOctokit(installationId: number | null) {
7 | let auth = ""
8 |
9 | if (process.env.NEXT_PUBLIC_APP_MODE === "simple") {
10 | auth = process.env.GITHUB_PAT!
11 | } else {
12 | const appAuth = createAppAuth({
13 | appId: process.env.NEXT_PUBLIC_GITHUB_APP_ID!,
14 | privateKey: process.env.GITHUB_PRIVATE_KEY!,
15 | clientId: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID!,
16 | clientSecret: process.env.GITHUB_CLIENT_SECRET!
17 | })
18 |
19 | if (!installationId) {
20 | throw new Error("Installation ID is required for app authentication")
21 | }
22 |
23 | const { token } = await appAuth({ type: "installation", installationId })
24 | auth = token
25 | }
26 |
27 | return new Octokit({ auth })
28 | }
29 |
--------------------------------------------------------------------------------
/components/profiles/profile-creator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createProject, createWorkspace } from "@/db/queries"
4 | import { createProfile } from "@/db/queries/profiles-queries"
5 | import { useRouter } from "next/navigation"
6 | import { useEffect } from "react"
7 |
8 | export const ProfileCreator = async () => {
9 | const router = useRouter()
10 |
11 | useEffect(() => {
12 | const handleCreateProfile = async () => {
13 | try {
14 | await createProfile({})
15 | const workspace = await createWorkspace({ name: "My Workspace" })
16 | const project = await createProject({
17 | name: "My Project",
18 | workspaceId: workspace.id
19 | })
20 | router.push(`/${workspace.id}/${project.id}/issues`)
21 | } catch (error) {
22 | console.error(error)
23 | }
24 | }
25 |
26 | handleCreateProfile()
27 | }, [])
28 |
29 | return <>>
30 | }
31 |
--------------------------------------------------------------------------------
/components/workspaces/edit-workspace-button.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { Settings } from "lucide-react"
3 | import { useRouter } from "next/navigation"
4 | import { HTMLAttributes } from "react"
5 |
6 | interface EditWorkspaceButtonProps extends HTMLAttributes {
7 | workspaceId: string
8 | }
9 |
10 | export function EditWorkspaceButton({
11 | workspaceId,
12 | ...props
13 | }: EditWorkspaceButtonProps) {
14 | const router = useRouter()
15 |
16 | const handleClick = () => {
17 | router.push(`/${workspaceId}/edit`)
18 | }
19 |
20 | return (
21 |
22 |
26 |
27 |
Workspace Settings
28 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/actions/github/delete-pr.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getAuthenticatedOctokit } from "@/actions/github/auth"
4 | import { SelectProject } from "@/db/schema"
5 |
6 | export async function deleteGitHubPR(
7 | project: SelectProject,
8 | prLink: string,
9 | branchName: string
10 | ) {
11 | const octokit = await getAuthenticatedOctokit(project.githubInstallationId!)
12 | const [owner, repo] = project.githubRepoFullName!.split("/")
13 |
14 | // Extract PR number from the link
15 | const prNumber = parseInt(prLink.split("/").pop() || "", 10)
16 |
17 | if (isNaN(prNumber)) {
18 | throw new Error("Invalid PR link")
19 | }
20 |
21 | // Close the PR
22 | await octokit.pulls.update({
23 | owner,
24 | repo,
25 | pull_number: prNumber,
26 | state: "closed"
27 | })
28 |
29 | // Delete the branch
30 | await octokit.git.deleteRef({
31 | owner,
32 | repo,
33 | ref: `heads/${branchName}`
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/components/dashboard/reusable/crud-page.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowLeft } from "lucide-react"
2 | import Link from "next/link"
3 | import { FC, ReactNode } from "react"
4 |
5 | interface CRUDPageProps {
6 | pageTitle: string
7 | backText: string
8 | backLink: string
9 | children: ReactNode
10 | }
11 |
12 | export const CRUDPage: FC = ({
13 | pageTitle,
14 | backText,
15 | backLink,
16 | children
17 | }) => {
18 | return (
19 |
20 |
24 |
25 | {backText}
26 |
27 |
28 |
{pageTitle}
29 |
30 |
31 |
32 |
{children}
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/templates/template.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { deleteTemplate } from "@/db/queries/templates-queries"
4 | import { SelectInstruction, SelectTemplate } from "@/db/schema"
5 | import { FC } from "react"
6 | import { InstructionTemplateView } from "../dashboard/reusable/instruction-template-view"
7 |
8 | interface TemplateProps {
9 | template: SelectTemplate & {
10 | templatesToInstructions: {
11 | templateId: string
12 | instructionId: string
13 | instruction: SelectInstruction
14 | }[]
15 | }
16 | }
17 |
18 | export const Template: FC = ({ template }) => {
19 | const attachedInstructions = template.templatesToInstructions.map(
20 | ti => ti.instruction
21 | )
22 |
23 | return (
24 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/actions/linear/webhook.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LinearWebhookBody,
3 | LinearWebhookComment,
4 | LinearWebhookIssue
5 | } from "@/types/linear/linear"
6 | import { LinearClient } from "@linear/sdk"
7 | import { handleCommentWebhook } from "./comments"
8 | import { handleIssueWebhook } from "./issues"
9 |
10 | export async function handleWebhook(
11 | linearClient: LinearClient,
12 | body: LinearWebhookBody
13 | ) {
14 | const { type, action, data, organizationId } = body
15 |
16 | switch (type) {
17 | case "Issue":
18 | await handleIssueWebhook(
19 | linearClient,
20 | action,
21 | data as LinearWebhookIssue,
22 | organizationId
23 | )
24 | break
25 | case "Comment":
26 | await handleCommentWebhook(
27 | linearClient,
28 | action,
29 | data as LinearWebhookComment
30 | )
31 | break
32 | default:
33 | console.error("Unused type", type)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/db/schema/workspaces-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
3 | import { projectsTable } from "./projects-schema"
4 |
5 | export const workspacesTable = pgTable("workspaces", {
6 | id: uuid("id").primaryKey().defaultRandom(),
7 | userId: text("user_id").notNull(),
8 | name: text("name").notNull(),
9 | linearAccessToken: text("linear_access_token"),
10 | linearOrganizationId: text("linear_organization_id"),
11 | createdAt: timestamp("created_at").notNull().defaultNow(),
12 | updatedAt: timestamp("updated_at")
13 | .notNull()
14 | .defaultNow()
15 | .$onUpdate(() => new Date())
16 | })
17 |
18 | export const workspacesRelations = relations(workspacesTable, ({ many }) => ({
19 | projects: many(projectsTable)
20 | }))
21 |
22 | export type InsertWorkspace = typeof workspacesTable.$inferInsert
23 | export type SelectWorkspace = typeof workspacesTable.$inferSelect
24 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/integrations/page.tsx:
--------------------------------------------------------------------------------
1 | import { Integrations } from "@/components/integrations/integrations"
2 | import { NotFound } from "@/components/utility/not-found"
3 | import { getProjectById, getWorkspaceById } from "@/db/queries"
4 |
5 | export default async function IntegrationsPage({
6 | params
7 | }: {
8 | params: { projectId: string; workspaceId: string }
9 | }) {
10 | const workspace = await getWorkspaceById(params.workspaceId)
11 | const project = await getProjectById(params.projectId)
12 |
13 | if (!workspace) {
14 | return
15 | }
16 |
17 | if (!project) {
18 | return
19 | }
20 |
21 | const isGitHubConnected = !!project.githubInstallationId
22 | const isLinearConnected = !!workspace.linearAccessToken
23 |
24 | return (
25 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/app/[workspaceId]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Dashboard } from "@/components/dashboard/dashboard"
2 | import { getProjectsByWorkspaceId } from "@/db/queries/projects-queries"
3 | import { getWorkspacesByUserId } from "@/db/queries/workspaces-queries"
4 |
5 | export const revalidate = 0
6 |
7 | export default async function WorkspaceLayout({
8 | children,
9 | params
10 | }: {
11 | children: React.ReactNode
12 | params: { workspaceId: string; projectId: string }
13 | }) {
14 | const workspaces = await getWorkspacesByUserId()
15 | const projects = await getProjectsByWorkspaceId(params.workspaceId)
16 |
17 | const IntegrationStatus = {
18 | isGitHubConnected: false,
19 | isLinearConnected: false
20 | }
21 |
22 | return (
23 |
30 | {children}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Setup Config ------------------------
2 |
3 | # App Mode
4 | NEXT_PUBLIC_APP_MODE=simple # simple or advanced
5 |
6 | # LLMs
7 | ANTHROPIC_API_KEY= # Needed for codegen
8 | OPENAI_API_KEY= # Needed for embeddings
9 |
10 | # DB
11 | DATABASE_URL=
12 |
13 | # Simple Settings ------------------------
14 |
15 | # GitHub
16 | GITHUB_PAT=
17 |
18 | # Pro Settings ------------------------
19 |
20 | # Auth
21 | CLERK_SECRET_KEY=
22 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
23 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login
24 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/signup
25 | NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/projects
26 | NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/onboarding
27 |
28 | # GitHub
29 | GITHUB_CLIENT_SECRET=
30 | GITHUB_PRIVATE_KEY=
31 | NEXT_PUBLIC_GITHUB_APP_NAME=
32 | NEXT_PUBLIC_GITHUB_APP_ID=
33 | NEXT_PUBLIC_GITHUB_CLIENT_ID=
34 |
35 | # Linear
36 | LINEAR_CLIENT_SECRET=
37 | LINEAR_WEBHOOK_SECRET=
38 | NEXT_PUBLIC_LINEAR_CLIENT_ID=
39 |
40 | # Webhooks (for local use)
41 | PORT=3000
42 | WEBHOOK_URL=
--------------------------------------------------------------------------------
/components/instructions/new-instruction-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createInstructionRecords } from "@/db/queries/instructions-queries"
4 | import { useParams, useRouter } from "next/navigation"
5 | import { CRUDForm } from "../dashboard/reusable/crud-form"
6 |
7 | export default function NewInstructionForm() {
8 | const params = useParams()
9 | const router = useRouter()
10 |
11 | const projectId = params.projectId as string
12 |
13 | const handleCreateInstruction = async (formData: FormData) => {
14 | const newInstruction = {
15 | name: formData.get("name") as string,
16 | content: formData.get("content") as string,
17 | projectId
18 | }
19 | const instruction = await createInstructionRecords([newInstruction])
20 | router.refresh()
21 | router.push(`../instructions/${instruction[0].id}`)
22 | }
23 |
24 | return (
25 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | useTabs: false,
6 | singleQuote: false,
7 | arrowParens: "avoid",
8 | tabWidth: 2,
9 | trailingComma: "none",
10 | importOrder: [
11 | "^.+\\.scss$",
12 | "^.+\\.css$",
13 | "^(react/(.*)$)|^(react$)",
14 | "^(next/(.*)$)|^(next$)",
15 | "",
16 | "",
17 | "^types$",
18 | "^@/types/(.*)$",
19 | "^@/config/(.*)$",
20 | "^@/lib/(.*)$",
21 | "^@/hooks/(.*)$",
22 | "^@/components/ui/(.*)$",
23 | "^@/components/(.*)$",
24 | "^@/registry/(.*)$",
25 | "^@/styles/(.*)$",
26 | "^@/app/(.*)$",
27 | "",
28 | "^[./]"
29 | ],
30 | importOrderSeparation: false,
31 | importOrderSortSpecifiers: true,
32 | importOrderBuiltinModulesToTop: true,
33 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
34 | importOrderMergeDuplicateImports: true,
35 | importOrderCombineTypeAndValueImports: true
36 | }
37 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
2 | import { NextResponse } from "next/server"
3 |
4 | const isSimpleMode = process.env.NEXT_PUBLIC_APP_MODE === "simple"
5 |
6 | const isProtectedRoute = createRouteMatcher(["/onboarding(.*), /projects(.*)"])
7 | const uuidRegex =
8 | /^\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(\/.*)?$/
9 |
10 | export default async function middleware() {
11 | if (isSimpleMode) {
12 | return NextResponse.next()
13 | }
14 |
15 | return clerkMiddleware(async (auth, req) => {
16 | const { userId, redirectToSignIn } = auth()
17 | const path = req.nextUrl.pathname
18 |
19 | const isProtected = isProtectedRoute(req) || uuidRegex.test(path)
20 |
21 | if (!userId && isProtected) {
22 | return redirectToSignIn({ returnBackUrl: "/login" })
23 | }
24 |
25 | if (userId && isProtected) {
26 | return NextResponse.next()
27 | }
28 | })
29 | }
30 |
31 | export const config = {
32 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"]
33 | }
34 |
--------------------------------------------------------------------------------
/db/schema/issue-messages-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
3 | import { issuesTable } from "./issues-schema"
4 |
5 | export const issueMessagesTable = pgTable("issue_messages", {
6 | id: uuid("id").primaryKey().defaultRandom(),
7 | issueId: uuid("issue_id")
8 | .notNull()
9 | .references(() => issuesTable.id, { onDelete: "cascade" }),
10 | content: text("content").notNull(),
11 | sequence: integer("sequence").notNull(),
12 | createdAt: timestamp("created_at").notNull().defaultNow(),
13 | updatedAt: timestamp("updated_at")
14 | .notNull()
15 | .defaultNow()
16 | .$onUpdate(() => new Date())
17 | })
18 |
19 | export const issueMessagesRelations = relations(
20 | issueMessagesTable,
21 | ({ one }) => ({
22 | issue: one(issuesTable, {
23 | fields: [issueMessagesTable.issueId],
24 | references: [issuesTable.id]
25 | })
26 | })
27 | )
28 |
29 | export type InsertIssueMessage = typeof issueMessagesTable.$inferInsert
30 | export type SelectIssueMessage = typeof issueMessagesTable.$inferSelect
31 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mckay Wrigley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/templates/[id]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import { CRUDPage } from "@/components/dashboard/reusable/crud-page"
2 | import EditTemplateForm from "@/components/templates/edit-template-form"
3 | import { NotFound } from "@/components/utility/not-found"
4 | import { getInstructionsByProjectId } from "@/db/queries/instructions-queries"
5 | import { getTemplateWithInstructionById } from "@/db/queries/templates-queries"
6 |
7 | export const revalidate = 0
8 |
9 | export default async function EditTemplatePage({
10 | params
11 | }: {
12 | params: { id: string; projectId: string }
13 | }) {
14 | const template = await getTemplateWithInstructionById(params.id)
15 |
16 | if (!template) {
17 | return
18 | }
19 |
20 | const instructions = await getInstructionsByProjectId(params.projectId)
21 |
22 | return (
23 |
28 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended",
8 | "plugin:@typescript-eslint/recommended"
9 | ],
10 | "plugins": ["tailwindcss", "@typescript-eslint"],
11 | "rules": {
12 | "@next/next/no-img-element": "off",
13 | "jsx-a11y/alt-text": "off",
14 | "react-hooks/exhaustive-deps": "off",
15 | "tailwindcss/enforces-negative-arbitrary-values": "off",
16 | "tailwindcss/no-contradicting-classname": "off",
17 | "tailwindcss/no-custom-classname": "off",
18 | "react/no-unescaped-entities": "off",
19 | "@typescript-eslint/no-unused-vars": [
20 | "error",
21 | { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
22 | ],
23 | "@typescript-eslint/no-explicit-any": "off"
24 | },
25 | "settings": {
26 | "tailwindcss": {
27 | "callees": ["cn", "cva"],
28 | "config": "tailwind.config.js"
29 | }
30 | },
31 | "overrides": [
32 | {
33 | "files": ["*.ts", "*.tsx"],
34 | "parser": "@typescript-eslint/parser"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/issues/[issueId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { IssueView } from "@/components/issues/issue-view"
2 | import { NotFound } from "@/components/utility/not-found"
3 | import { getIssueById } from "@/db/queries/issues-queries"
4 | import { getInstructionsForIssue } from "@/db/queries/issues-to-instructions-queries"
5 | import { getProjectById } from "@/db/queries/projects-queries"
6 |
7 | export const revalidate = 0
8 |
9 | export default async function IssuePage({
10 | params
11 | }: {
12 | params: { issueId: string; projectId: string; workspaceId: string }
13 | }) {
14 | const issue = await getIssueById(params.issueId)
15 |
16 | if (!issue) {
17 | return
18 | }
19 |
20 | const project = await getProjectById(issue.projectId)
21 |
22 | if (!project) {
23 | return
24 | }
25 |
26 | const attachedInstructions = await getInstructionsForIssue(issue.id)
27 |
28 | return (
29 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/actions/ai/generate-ai-response.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { calculateLLMCost } from "@/lib/ai/calculate-llm-cost"
4 | import { BUILDWARE_MAX_OUTPUT_TOKENS } from "@/lib/constants/buildware-config"
5 | import Anthropic from "@anthropic-ai/sdk"
6 |
7 | const anthropic = new Anthropic()
8 |
9 | export const generateAIResponse = async (
10 | messages: Anthropic.Messages.MessageParam[]
11 | ) => {
12 | const message = await anthropic.messages.create(
13 | {
14 | model: "claude-3-5-sonnet-20240620",
15 | system:
16 | "You are a helpful assistant that can answer questions and help with tasks.",
17 | messages,
18 | max_tokens: BUILDWARE_MAX_OUTPUT_TOKENS
19 | },
20 | {
21 | headers: {
22 | "anthropic-beta": "max-tokens-3-5-sonnet-2024-07-15"
23 | }
24 | }
25 | )
26 |
27 | console.warn("usage", message.usage)
28 | const cost = calculateLLMCost({
29 | llmId: "claude-3-5-sonnet-20240620",
30 | inputTokens: message.usage.input_tokens,
31 | outputTokens: message.usage.output_tokens
32 | })
33 | console.warn("cost", cost)
34 |
35 | return message.content[0].type === "text" ? message.content[0].text : ""
36 | }
37 |
--------------------------------------------------------------------------------
/components/templates/handle-save.tsx:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import {
4 | addInstructionToTemplate,
5 | getInstructionsForTemplate,
6 | removeInstructionFromTemplate
7 | } from "@/db/queries/templates-to-instructions-queries"
8 |
9 | export async function updateTemplateInstructions(
10 | templateId: string,
11 | selectedInstructions: string[]
12 | ) {
13 | try {
14 | const currentInstructions = await getInstructionsForTemplate(templateId)
15 | const currentInstructionIds = currentInstructions.map(p => p.instructionId)
16 |
17 | for (const instructionId of currentInstructionIds) {
18 | if (!selectedInstructions.includes(instructionId)) {
19 | await removeInstructionFromTemplate(templateId, instructionId)
20 | }
21 | }
22 |
23 | for (const instructionId of selectedInstructions) {
24 | if (!currentInstructionIds.includes(instructionId)) {
25 | await addInstructionToTemplate(templateId, instructionId)
26 | }
27 | }
28 |
29 | return { success: true }
30 | } catch (error) {
31 | console.error("Error saving instructions:", error)
32 | return { success: false, error: "Failed to update template instructions" }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
4 | import { Check } from "lucide-react"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as SliderPrimitive from "@radix-ui/react-slider"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/db/queries/embedded-files-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getUserId } from "@/actions/auth/auth"
4 | import { eq } from "drizzle-orm"
5 | import { revalidatePath } from "next/cache"
6 | import { db } from "../db"
7 | import {
8 | InsertEmbeddedFile,
9 | embeddedFilesTable
10 | } from "../schema/embedded-files-schema"
11 |
12 | export async function createEmbeddedFiles(
13 | data: Omit[]
14 | ) {
15 | const userId = await getUserId()
16 |
17 | try {
18 | await db.insert(embeddedFilesTable).values(
19 | data.map(file => ({
20 | ...file,
21 | userId
22 | }))
23 | )
24 | revalidatePath("/")
25 | } catch (error) {
26 | console.error("Error inserting records into embedded_files:", error)
27 | throw error
28 | }
29 | }
30 |
31 | export async function deleteAllEmbeddedFilesByEmbeddedBranchId(
32 | embeddedBranchId: string
33 | ) {
34 | try {
35 | await db
36 | .delete(embeddedFilesTable)
37 | .where(eq(embeddedFilesTable.embeddedBranchId, embeddedBranchId))
38 | revalidatePath("/")
39 | } catch (error) {
40 | console.error("Error deleting records from embedded_files:", error)
41 | throw error
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/components/instructions/edit-instruction-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { updateInstruction } from "@/db/queries/instructions-queries"
4 | import { SelectInstruction } from "@/db/schema"
5 | import { useRouter } from "next/navigation"
6 | import { CRUDForm } from "../dashboard/reusable/crud-form"
7 |
8 | export default function EditInstructionForm({
9 | instruction
10 | }: {
11 | instruction: SelectInstruction
12 | }) {
13 | const router = useRouter()
14 |
15 | const handleUpdateInstruction = async (formData: FormData) => {
16 | try {
17 | const updatedInstruction = {
18 | title: formData.get("title") as string,
19 | content: formData.get("content") as string
20 | }
21 | await updateInstruction(instruction.id, updatedInstruction)
22 | router.refresh()
23 | router.push(`../${instruction.id}`)
24 | } catch (error) {
25 | console.error("Failed to update instruction:", error)
26 | }
27 | }
28 |
29 | return (
30 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/issues/issues-list.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { deleteIssue } from "@/db/queries/issues-queries"
4 | import { SelectIssue } from "@/db/schema/issues-schema"
5 | import { DataItem } from "../dashboard/reusable/data-item"
6 | import { DataList } from "../dashboard/reusable/data-list"
7 |
8 | interface IssuesListProps {
9 | issues: SelectIssue[]
10 | }
11 |
12 | export function IssuesList({ issues }: IssuesListProps) {
13 | const handleIssueDelete = async (id: string) => {
14 | await deleteIssue(id)
15 | }
16 |
17 | return (
18 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/lib/ai/limit-tokens.ts:
--------------------------------------------------------------------------------
1 | import { BUILDWARE_MAX_INPUT_TOKENS } from "../constants/buildware-config"
2 | import { estimateClaudeSonnet3_5TokenCount } from "./estimate-claude-tokens"
3 |
4 | export function limitTokens(
5 | basePrompt: string,
6 | files: { path: string; content: string }[]
7 | ): { prompt: string; includedFiles: typeof files; tokensUsed: number } {
8 | let totalTokens = estimateClaudeSonnet3_5TokenCount(basePrompt)
9 | const includedFiles: typeof files = []
10 |
11 | for (const file of files) {
12 | const fileContent = `# File Path: ${file.path}\n${file.content}`
13 | const fileTokens = estimateClaudeSonnet3_5TokenCount(fileContent)
14 |
15 | if (totalTokens + fileTokens <= BUILDWARE_MAX_INPUT_TOKENS) {
16 | includedFiles.push(file)
17 | totalTokens += fileTokens
18 | } else {
19 | break
20 | }
21 | }
22 |
23 | const codebaseContext =
24 | includedFiles.length > 0
25 | ? `# Available Codebase Files\n${includedFiles.map(file => `## File Path: ${file.path}\n${file.content}`).join("\n\n")}`
26 | : "No codebase files."
27 |
28 | const prompt = `${basePrompt}${codebaseContext}`
29 | return {
30 | prompt,
31 | includedFiles,
32 | tokensUsed: totalTokens
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as SwitchPrimitives from "@radix-ui/react-switch"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from "class-variance-authority"
2 | import * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
13 | secondary:
14 | "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
15 | destructive:
16 | "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
17 | outline: "text-foreground"
18 | }
19 | },
20 | defaultVariants: {
21 | variant: "default"
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
31 |
--------------------------------------------------------------------------------
/db/schema/embedded-branches-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
3 | import { projectsTable } from "./projects-schema"
4 |
5 | export const embeddedBranchesTable = pgTable("embedded_branches", {
6 | id: uuid("id").primaryKey().defaultRandom(),
7 | userId: text("user_id").notNull(),
8 | projectId: uuid("project_id")
9 | .notNull()
10 | .references(() => projectsTable.id, { onDelete: "cascade" }),
11 | githubRepoFullName: text("github_repo_full_name").notNull(),
12 | branchName: text("branch_name").notNull(),
13 | lastEmbeddedCommitHash: text("last_embedded_commit_hash"),
14 | createdAt: timestamp("created_at").notNull().defaultNow(),
15 | updatedAt: timestamp("updated_at")
16 | .notNull()
17 | .defaultNow()
18 | .$onUpdate(() => new Date())
19 | })
20 |
21 | export const embeddedBranchesRelations = relations(
22 | embeddedBranchesTable,
23 | ({ one }) => ({
24 | project: one(projectsTable, {
25 | fields: [embeddedBranchesTable.projectId],
26 | references: [projectsTable.id]
27 | })
28 | })
29 | )
30 |
31 | export type InsertEmbeddedBranch = typeof embeddedBranchesTable.$inferInsert
32 | export type SelectEmbeddedBranch = typeof embeddedBranchesTable.$inferSelect
33 |
--------------------------------------------------------------------------------
/app/[workspaceId]/[projectId]/templates/page.tsx:
--------------------------------------------------------------------------------
1 | import { TemplatesList } from "@/components/templates/template-list"
2 | import { getInstructionsByProjectId } from "@/db/queries/instructions-queries"
3 | import { getTemplatesWithInstructionsByProjectId } from "@/db/queries/templates-queries"
4 | import { SelectInstruction, SelectTemplate } from "@/db/schema"
5 |
6 | export const revalidate = 0
7 |
8 | export default async function TemplatesPage({
9 | params
10 | }: {
11 | params: { projectId: string }
12 | }) {
13 | let templatesWithInstructions: (SelectTemplate & {
14 | templatesToInstructions: {
15 | templateId: string
16 | instructionId: string
17 | instruction: SelectInstruction
18 | }[]
19 | })[] = []
20 | let instructions: SelectInstruction[] = []
21 |
22 | const { projectId } = params
23 |
24 | try {
25 | templatesWithInstructions =
26 | await getTemplatesWithInstructionsByProjectId(projectId)
27 | instructions = await getInstructionsByProjectId(projectId)
28 | } catch (error) {
29 | console.error("Error fetching data:", error)
30 | }
31 |
32 | return (
33 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/components/integrations/integration.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { Loader2 } from "lucide-react"
3 | import { FC, HTMLAttributes, ReactNode } from "react"
4 | import { Button } from "../ui/button"
5 |
6 | interface IntegrationProps extends HTMLAttributes {
7 | name: string
8 | icon: ReactNode
9 | isConnecting: boolean
10 | isConnected: boolean
11 | disabled: boolean
12 | onClick: () => void
13 | }
14 |
15 | export const Integration: FC = ({
16 | name,
17 | icon,
18 | isConnecting,
19 | isConnected,
20 | disabled,
21 | onClick,
22 | ...props
23 | }) => {
24 | return (
25 |
31 |
32 |
{icon}
33 |
{name}
34 |
35 |
36 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardContent, HoverCardTrigger }
30 |
--------------------------------------------------------------------------------
/actions/linear/issues.ts:
--------------------------------------------------------------------------------
1 | import { LinearWebhookIssue } from "@/types/linear/linear"
2 | import { LinearClient } from "@linear/sdk"
3 | import { checkForAILabel } from "./labels"
4 |
5 | export async function handleIssueWebhook(
6 | linearClient: LinearClient,
7 | action: string,
8 | data: LinearWebhookIssue,
9 | organizationId: string
10 | ) {
11 | console.warn("organizationId", organizationId)
12 |
13 | const issue = await linearClient.issue(data.id)
14 | console.warn("issue", issue)
15 |
16 | const hasAILabel = await checkForAILabel(data.labels.map(label => label.name))
17 |
18 | if (hasAILabel) {
19 | switch (action) {
20 | case "create":
21 | return
22 | // await handleAILabelAssignment(linearClient, issue, organizationId)
23 | break
24 | case "update":
25 | if (data.createdAt === data.updatedAt) {
26 | // Skip the double trigger on a create
27 | console.error("skipped double trigger")
28 | return
29 | }
30 | return
31 | // await handleAILabelAssignment(linearClient, issue, organizationId)
32 | break
33 | default:
34 | console.error("Unknown issue action", action)
35 | }
36 | } else {
37 | // Doesn't have an AI label, skip
38 | console.error("no AI label found")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/db/schema/templates-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
3 | import { issuesTable } from "./issues-schema"
4 | import { projectsTable } from "./projects-schema"
5 | import { templatesToInstructionsTable } from "./templates-to-instructions-schema"
6 |
7 | export const templatesTable = pgTable("templates", {
8 | id: uuid("id").primaryKey().defaultRandom(),
9 | userId: text("user_id").notNull(),
10 | projectId: uuid("project_id")
11 | .notNull()
12 | .references(() => projectsTable.id, { onDelete: "cascade" }),
13 | name: text("name").notNull(),
14 | content: text("content").notNull(),
15 | createdAt: timestamp("created_at").notNull().defaultNow(),
16 | updatedAt: timestamp("updated_at")
17 | .notNull()
18 | .defaultNow()
19 | .$onUpdate(() => new Date())
20 | })
21 |
22 | export const templateRelations = relations(templatesTable, ({ one, many }) => ({
23 | templatesToInstructions: many(templatesToInstructionsTable),
24 | issues: many(issuesTable),
25 | project: one(projectsTable, {
26 | fields: [templatesTable.projectId],
27 | references: [projectsTable.id]
28 | })
29 | }))
30 |
31 | export type InsertTemplate = typeof templatesTable.$inferInsert
32 | export type SelectTemplate = typeof templatesTable.$inferSelect
33 |
--------------------------------------------------------------------------------
/db/schema/issues-to-instructions-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { pgTable, primaryKey, uuid } from "drizzle-orm/pg-core"
3 | import { instructionsTable } from "./instructions-schema"
4 | import { issuesTable } from "./issues-schema"
5 |
6 | export const issuesToInstructionsTable = pgTable(
7 | "issues_to_instructions",
8 | {
9 | issueId: uuid("issue_id")
10 | .notNull()
11 | .references(() => issuesTable.id, { onDelete: "cascade" }),
12 | instructionId: uuid("instruction_id")
13 | .notNull()
14 | .references(() => instructionsTable.id, { onDelete: "cascade" })
15 | },
16 | t => ({
17 | pk: primaryKey({ columns: [t.issueId, t.instructionId] })
18 | })
19 | )
20 |
21 | export const issueToInstructionsRelations = relations(
22 | issuesToInstructionsTable,
23 | ({ one }) => ({
24 | issue: one(issuesTable, {
25 | fields: [issuesToInstructionsTable.issueId],
26 | references: [issuesTable.id]
27 | }),
28 | instruction: one(instructionsTable, {
29 | fields: [issuesToInstructionsTable.instructionId],
30 | references: [instructionsTable.id]
31 | })
32 | })
33 | )
34 |
35 | export type InsertIssuesToInstruction =
36 | typeof issuesToInstructionsTable.$inferInsert
37 | export type SelectIssuesToInstruction =
38 | typeof issuesToInstructionsTable.$inferSelect
39 |
--------------------------------------------------------------------------------
/components/projects/create-project-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createProject } from "@/db/queries/projects-queries"
4 | import { cn } from "@/lib/utils"
5 | import { PlusIcon } from "lucide-react"
6 | import { useRouter } from "next/navigation"
7 | import { FC, HTMLAttributes } from "react"
8 |
9 | interface CreateProjectButtonProps extends HTMLAttributes {
10 | params: {
11 | workspaceId: string
12 | }
13 | }
14 |
15 | export const CreateProjectButton: FC = ({
16 | params,
17 | ...props
18 | }) => {
19 | const router = useRouter()
20 |
21 | const handleCreateProject = async () => {
22 | try {
23 | const project = await createProject({
24 | name: "New Project",
25 | workspaceId: params.workspaceId
26 | })
27 | router.refresh()
28 | router.push(`/${params.workspaceId}/${project.id}/settings`)
29 | } catch (error) {
30 | console.error(error)
31 | }
32 | }
33 |
34 | return (
35 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as PopoverPrimitive from "@radix-ui/react-popover"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverContent, PopoverTrigger }
32 |
--------------------------------------------------------------------------------
/components/instructions/instruction-list.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { deleteInstruction } from "@/db/queries/instructions-queries"
4 | import { SelectInstruction } from "@/db/schema"
5 | import { FC } from "react"
6 | import { DataItem } from "../dashboard/reusable/data-item"
7 | import { DataList } from "../dashboard/reusable/data-list"
8 |
9 | interface InstructionsListProps {
10 | instructions: SelectInstruction[]
11 | }
12 |
13 | export const InstructionsList: FC = ({
14 | instructions
15 | }) => {
16 | const handleDelete = async (id: string) => {
17 | await deleteInstruction(id)
18 | }
19 | return (
20 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/app/api/auth/callback/github/route.ts:
--------------------------------------------------------------------------------
1 | import { getProjectById, updateProject } from "@/db/queries/projects-queries"
2 | import { revalidatePath } from "next/cache"
3 | import { redirect } from "next/navigation"
4 |
5 | export async function GET(req: Request) {
6 | const { searchParams } = new URL(req.url)
7 | const installationId = parseInt(searchParams.get("installation_id")!)
8 | const state = searchParams.get("state")
9 |
10 | if (!installationId || !state) {
11 | return new Response(
12 | JSON.stringify({ error: "Missing installation ID or state" }),
13 | {
14 | status: 400,
15 | headers: { "Content-Type": "application/json" }
16 | }
17 | )
18 | }
19 |
20 | const { projectId } = JSON.parse(decodeURIComponent(state))
21 |
22 | let project = null
23 | try {
24 | try {
25 | project = await getProjectById(projectId)
26 |
27 | if (!project) {
28 | throw new Error("Project not found")
29 | }
30 |
31 | await updateProject(project.id, {
32 | githubInstallationId: installationId
33 | })
34 |
35 | revalidatePath(`/`)
36 | } catch (error) {
37 | console.error("Error authenticating:", error)
38 | }
39 | } catch (error: any) {
40 | console.error("Error in GitHub callback:", error)
41 | }
42 |
43 | return redirect(`/${project?.workspaceId}/${project?.id}/settings`)
44 | }
45 |
--------------------------------------------------------------------------------
/db/schema/templates-to-instructions-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { pgTable, primaryKey, uuid } from "drizzle-orm/pg-core"
3 | import { instructionsTable } from "./instructions-schema"
4 | import { templatesTable } from "./templates-schema"
5 |
6 | export const templatesToInstructionsTable = pgTable(
7 | "templates_to_instructions",
8 | {
9 | templateId: uuid("template_id")
10 | .notNull()
11 | .references(() => templatesTable.id, { onDelete: "cascade" }),
12 | instructionId: uuid("instruction_id")
13 | .notNull()
14 | .references(() => instructionsTable.id, { onDelete: "cascade" })
15 | },
16 | t => ({
17 | pk: primaryKey({ columns: [t.templateId, t.instructionId] })
18 | })
19 | )
20 |
21 | export const templateToInstructionsRelations = relations(
22 | templatesToInstructionsTable,
23 | ({ one }) => ({
24 | template: one(templatesTable, {
25 | fields: [templatesToInstructionsTable.templateId],
26 | references: [templatesTable.id]
27 | }),
28 | instruction: one(instructionsTable, {
29 | fields: [templatesToInstructionsTable.instructionId],
30 | references: [instructionsTable.id]
31 | })
32 | })
33 | )
34 |
35 | export type InsertTemplatesToInstruction =
36 | typeof templatesToInstructionsTable.$inferInsert
37 | export type SelectTemplatesToInstruction =
38 | typeof templatesToInstructionsTable.$inferSelect
39 |
--------------------------------------------------------------------------------
/db/queries/profiles-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getUserId } from "@/actions/auth/auth"
4 | import console from "console"
5 | import { eq } from "drizzle-orm"
6 | import { revalidatePath } from "next/cache"
7 | import { db } from "../db"
8 | import {
9 | InsertProfile,
10 | SelectProfile,
11 | profilesTable
12 | } from "../schema/profiles-schema"
13 |
14 | export async function createProfile(
15 | data: Partial
16 | ): Promise {
17 | const userId = await getUserId()
18 |
19 | const [profile] = await db
20 | .insert(profilesTable)
21 | .values({ ...data, userId })
22 | .returning()
23 |
24 | revalidatePath("/")
25 | return profile
26 | }
27 |
28 | export async function getProfileByUserId(): Promise {
29 | const userId = await getUserId()
30 |
31 | try {
32 | const profile = await db.query.profiles.findFirst({
33 | where: eq(profilesTable.userId, userId)
34 | })
35 | return profile
36 | } catch (error) {
37 | console.error(error)
38 | }
39 | }
40 |
41 | export async function updateProfile(
42 | data: Partial
43 | ): Promise {
44 | const userId = await getUserId()
45 |
46 | const [updatedProfile] = await db
47 | .update(profilesTable)
48 | .set(data)
49 | .where(eq(profilesTable.userId, userId))
50 | .returning()
51 | revalidatePath("/")
52 | return updatedProfile
53 | }
54 |
--------------------------------------------------------------------------------
/db/schema/issues-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
3 | import { issueMessagesTable } from "./issue-messages-schema"
4 | import { issuesToInstructionsTable } from "./issues-to-instructions-schema"
5 | import { projectsTable } from "./projects-schema"
6 |
7 | export const issuesTable = pgTable("issues", {
8 | id: uuid("id").primaryKey().defaultRandom(),
9 | userId: text("user_id").notNull(),
10 | projectId: uuid("project_id")
11 | .notNull()
12 | .references(() => projectsTable.id, { onDelete: "cascade" }),
13 | name: text("name").notNull(),
14 | content: text("content").notNull(),
15 | status: text("status").notNull().default("ready"),
16 | prLink: text("pr_link"),
17 | prBranch: text("pr_branch"),
18 | createdAt: timestamp("created_at").notNull().defaultNow(),
19 | updatedAt: timestamp("updated_at")
20 | .notNull()
21 | .defaultNow()
22 | .$onUpdate(() => new Date())
23 | })
24 |
25 | export const issuesRelations = relations(issuesTable, ({ one, many }) => ({
26 | project: one(projectsTable, {
27 | fields: [issuesTable.projectId],
28 | references: [projectsTable.id]
29 | }),
30 | issueToInstructions: many(issuesToInstructionsTable),
31 | issueMessages: many(issueMessagesTable)
32 | }))
33 |
34 | export type InsertIssue = typeof issuesTable.$inferInsert
35 | export type SelectIssue = typeof issuesTable.$inferSelect
36 |
--------------------------------------------------------------------------------
/db/schema/instructions-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
3 | import { issuesToInstructionsTable } from "./issues-to-instructions-schema"
4 | import { projectsTable } from "./projects-schema"
5 | import { templatesToInstructionsTable } from "./templates-to-instructions-schema"
6 |
7 | export const instructionsTable = pgTable("instructions", {
8 | id: uuid("id").primaryKey().defaultRandom(),
9 | userId: text("user_id").notNull(),
10 | projectId: uuid("project_id")
11 | .notNull()
12 | .references(() => projectsTable.id, { onDelete: "cascade" }),
13 | name: text("name").notNull(),
14 | content: text("content").notNull(),
15 | createdAt: timestamp("created_at").notNull().defaultNow(),
16 | updatedAt: timestamp("updated_at")
17 | .notNull()
18 | .defaultNow()
19 | .$onUpdate(() => new Date())
20 | })
21 |
22 | export const instructionsRelations = relations(
23 | instructionsTable,
24 | ({ one, many }) => ({
25 | templatesToInstructions: many(templatesToInstructionsTable),
26 | issueToInstructions: many(issuesToInstructionsTable),
27 | project: one(projectsTable, {
28 | fields: [instructionsTable.projectId],
29 | references: [projectsTable.id]
30 | })
31 | })
32 | )
33 |
34 | export type InsertInstruction = typeof instructionsTable.$inferInsert
35 | export type SelectInstruction = typeof instructionsTable.$inferSelect
36 |
--------------------------------------------------------------------------------
/db/schema/projects-schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm"
2 | import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
3 | import { instructionsTable } from "./instructions-schema"
4 | import { templatesTable } from "./templates-schema"
5 | import { workspacesTable } from "./workspaces-schema"
6 | import { issuesTable } from "./issues-schema"
7 |
8 | export const projectsTable = pgTable("projects", {
9 | id: uuid("id").primaryKey().defaultRandom(),
10 | userId: text("user_id").notNull(),
11 | workspaceId: uuid("workspace_id")
12 | .notNull()
13 | .references(() => workspacesTable.id, {
14 | onDelete: "cascade"
15 | }),
16 | name: text("name").notNull(),
17 | githubRepoFullName: text("github_repo_full_name"),
18 | githubTargetBranch: text("github_target_branch"),
19 | githubInstallationId: integer("github_installation_id"),
20 | createdAt: timestamp("created_at").notNull().defaultNow(),
21 | updatedAt: timestamp("updated_at")
22 | .notNull()
23 | .defaultNow()
24 | .$onUpdate(() => new Date())
25 | })
26 |
27 | export const projectsRelations = relations(projectsTable, ({ one, many }) => ({
28 | templates: many(templatesTable),
29 | instructions: many(instructionsTable),
30 | workspace: one(workspacesTable),
31 | issues: many(issuesTable)
32 | }))
33 |
34 | export type InsertProject = typeof projectsTable.$inferInsert
35 | export type SelectProject = typeof projectsTable.$inferSelect
36 |
--------------------------------------------------------------------------------
/actions/github/embed-files.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { InsertEmbeddedFile } from "@/db/schema"
4 | import {
5 | BUILDWARE_EMBEDDING_DIMENSIONS,
6 | BUILDWARE_EMBEDDING_MODEL
7 | } from "@/lib/constants/buildware-config"
8 | import { GitHubFileContent } from "@/types/github"
9 | import { encode } from "gpt-tokenizer"
10 | import OpenAI from "openai"
11 |
12 | const openai = new OpenAI()
13 |
14 | export async function embedFiles(filesContent: GitHubFileContent[]) {
15 | let embeddings: number[][] = []
16 |
17 | try {
18 | const response = await openai.embeddings.create({
19 | model: BUILDWARE_EMBEDDING_MODEL,
20 | dimensions: BUILDWARE_EMBEDDING_DIMENSIONS,
21 | // embed path + content
22 | input: filesContent.map(file => `${file.path}\n${file.content}`)
23 | })
24 |
25 | if (response && response.data) {
26 | embeddings = response.data.map(item => item.embedding)
27 | } else {
28 | console.error("OpenAI API call failed, response is undefined.")
29 | }
30 | } catch (error) {
31 | console.error("Error calling OpenAI API:", error)
32 | }
33 |
34 | const preparedFiles: Omit<
35 | InsertEmbeddedFile,
36 | "userId" | "projectId" | "embeddedBranchId" | "githubRepoFullName"
37 | >[] = filesContent.map((file, index) => ({
38 | path: file.path,
39 | content: file.content,
40 | tokenCount: encode(file.content).length,
41 | embedding: embeddings[index]
42 | }))
43 |
44 | return preparedFiles
45 | }
46 |
--------------------------------------------------------------------------------
/actions/github/list-repos.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { GitHubRepository } from "@/types/github"
4 | import { getAuthenticatedOctokit } from "./auth"
5 |
6 | export const listRepos = async (
7 | installationId: number | null
8 | ): Promise => {
9 | try {
10 | const octokit = await getAuthenticatedOctokit(installationId)
11 | let repositories: any[] = []
12 | let page = 1
13 | const per_page = 100 // Max allowed by GitHub API
14 |
15 | while (true) {
16 | let response: any
17 |
18 | if (installationId) {
19 | response = await octokit.apps.listReposAccessibleToInstallation({
20 | per_page,
21 | page
22 | })
23 | repositories = repositories.concat(response.data.repositories)
24 | } else {
25 | response = await octokit.request("GET /user/repos", {
26 | per_page,
27 | page
28 | })
29 | repositories = repositories.concat(response.data)
30 | }
31 |
32 | if (response.data.length < per_page) break
33 | page++
34 | }
35 |
36 | return repositories
37 | .map(repo => ({
38 | description: repo.description,
39 | full_name: repo.full_name,
40 | html_url: repo.html_url,
41 | id: repo.id,
42 | name: repo.name,
43 | private: repo.private
44 | }))
45 | .sort((a, b) => a.name.localeCompare(b.name))
46 | } catch (error: any) {
47 | throw new Error(error)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/components/templates/template-select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { SelectInstruction, SelectTemplate } from "@/db/schema"
4 | import { useState } from "react"
5 | import { MultiSelect } from "../ui/multi-select"
6 | import { updateTemplateInstructions } from "./handle-save"
7 |
8 | interface TemplateSelectProps {
9 | instructions: SelectInstruction[]
10 | templateWithInstructions: SelectTemplate & {
11 | templatesToInstructions: {
12 | templateId: string
13 | instructionId: string
14 | instruction: SelectInstruction
15 | }[]
16 | }
17 | }
18 |
19 | export function TemplateSelect({
20 | instructions,
21 | templateWithInstructions
22 | }: TemplateSelectProps) {
23 | const [selectedIds, setSelectedIds] = useState()
24 |
25 | if (!instructions || instructions.length === 0) {
26 | return No instructions selected for this template
27 | }
28 |
29 | const handleSelect = async (ids: string[]) => {
30 | setSelectedIds(ids)
31 | await updateTemplateInstructions(templateWithInstructions.id, ids)
32 | }
33 |
34 | return (
35 | ({
38 | id: instruction.id,
39 | name: instruction.name
40 | }))}
41 | selectedIds={
42 | selectedIds ||
43 | templateWithInstructions.templatesToInstructions.map(
44 | t => t.instructionId
45 | )
46 | }
47 | onToggleSelect={handleSelect}
48 | />
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/components/integrations/integrations.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useSearchParams } from "next/navigation"
4 | import { FC, useEffect, useState } from "react"
5 | import { ConnectGitHub } from "./connect-github"
6 | import { ConnectLinear } from "./connect-linear"
7 |
8 | interface IntegrationsProps {
9 | isGitHubConnected: boolean
10 | isLinearConnected: boolean
11 | }
12 |
13 | export const Integrations: FC = ({
14 | isGitHubConnected,
15 | isLinearConnected
16 | }) => {
17 | const searchParams = useSearchParams()
18 |
19 | const [error, setError] = useState("")
20 | const [success, setSuccess] = useState("")
21 |
22 | useEffect(() => {
23 | const errorParam = searchParams.get("error")
24 | if (errorParam) {
25 | setError(decodeURIComponent(errorParam))
26 | }
27 |
28 | const successParam = searchParams.get("success")
29 | if (successParam) {
30 | setSuccess(decodeURIComponent(successParam))
31 | }
32 | }, [searchParams])
33 |
34 | return (
35 |
36 |
Integrations
37 |
38 | {error &&
{error}
}
39 | {success &&
{success}
}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/components/marketing/main-section.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { ArrowRightIcon } from "@radix-ui/react-icons"
5 | import Link from "next/link"
6 | import ShineBorder from "../magicui/shine-border"
7 |
8 | export default function MainSection() {
9 | return (
10 |
11 |
12 | Ship faster
13 |
with Buildware
14 |
15 |
16 |
17 | Build a code instruction system,
18 | give it an issue,
19 | and get an AI-generated PR!
20 |
21 |
22 |
23 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/marketing/site-footer.tsx:
--------------------------------------------------------------------------------
1 | import { TwitterLogoIcon } from "@radix-ui/react-icons"
2 | import Link from "next/link"
3 |
4 | const footerSocials = [
5 | {
6 | href: "https://twitter.com/buildwareai",
7 | name: "Twitter",
8 | icon:
9 | }
10 | ]
11 |
12 | export function SiteFooter() {
13 | return (
14 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster } from "@/components/ui/sonner"
2 | import { ThemeProvider } from "@/components/utility/theme-provider"
3 | import { cn } from "@/lib/utils"
4 | import { ClerkProvider } from "@clerk/nextjs"
5 | import { dark } from "@clerk/themes"
6 | import type { Metadata } from "next"
7 | import { Inter as FontSans } from "next/font/google"
8 | import "./globals.css"
9 |
10 | const fontSans = FontSans({
11 | subsets: ["latin"],
12 | variable: "--font-sans"
13 | })
14 |
15 | export const metadata: Metadata = {
16 | title: "Buildware",
17 | description: "Build software with AI."
18 | }
19 |
20 | export default function RootLayout({
21 | children
22 | }: Readonly<{
23 | children: React.ReactNode
24 | }>) {
25 | const appMode = process.env.NEXT_PUBLIC_APP_MODE
26 |
27 | const content = (
28 |
29 |
35 |
40 | {children}
41 |
42 |
43 |
44 |
45 | )
46 |
47 | if (appMode === "simple") {
48 | return content
49 | }
50 |
51 | return (
52 |
57 | {content}
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/components/dashboard/reusable/data-list.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { FC, ReactNode } from "react"
5 | import { Button } from "../../ui/button"
6 |
7 | interface DataListProps {
8 | title: string
9 | subtitle: string
10 | readMoreLink: string
11 | readMoreText: string
12 | createLink: string
13 | createText: string
14 | dataListTitle: string
15 | children: ReactNode
16 | }
17 |
18 | export const DataList: FC = ({
19 | title,
20 | subtitle,
21 | readMoreLink,
22 | readMoreText,
23 | createLink,
24 | createText,
25 | dataListTitle,
26 | children
27 | }) => {
28 | return (
29 |
30 |
{title}
31 |
32 |
{subtitle}
33 |
34 |
35 |
40 | {readMoreText} →
41 |
42 |
43 |
44 |
45 |
46 |
47 |
{dataListTitle}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
{children}
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/components/integrations/connect-github.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Github } from "lucide-react"
4 | import { useParams, useRouter } from "next/navigation"
5 | import { FC, useState } from "react"
6 | import { Integration } from "./integration"
7 |
8 | interface ConnectGitHubProps {
9 | isGitHubConnected: boolean
10 | }
11 |
12 | export const ConnectGitHub: FC = ({
13 | isGitHubConnected
14 | }) => {
15 | const router = useRouter()
16 | const params = useParams()
17 |
18 | const [isConnecting, setIsConnecting] = useState(false)
19 |
20 | const projectId = params.projectId as string
21 |
22 | const handleConnect = async () => {
23 | try {
24 | if (!projectId) {
25 | throw new Error("Project ID not found")
26 | }
27 |
28 | setIsConnecting(true)
29 |
30 | const state = encodeURIComponent(JSON.stringify({ projectId }))
31 | const baseUrl = `https://github.com/apps/${process.env.NEXT_PUBLIC_GITHUB_APP_NAME}/installations/select_target`
32 | const githubUrl = `${baseUrl}?state=${state}`
33 |
34 | router.push(githubUrl)
35 | } catch (error) {
36 | console.error("GitHub connection error:", error)
37 | router.push(`/projects`)
38 | } finally {
39 | setIsConnecting(false)
40 | }
41 | }
42 |
43 | return (
44 | }
47 | isConnecting={isConnecting}
48 | isConnected={isGitHubConnected}
49 | disabled={isConnecting || isGitHubConnected}
50 | onClick={handleConnect}
51 | />
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/db/queries/issues-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getUserId } from "@/actions/auth/auth"
4 | import { and, desc, eq } from "drizzle-orm"
5 | import { revalidatePath } from "next/cache"
6 | import { db } from "../db"
7 | import { InsertIssue, SelectIssue, issuesTable } from "../schema/issues-schema"
8 |
9 | export async function createIssue(
10 | data: Omit
11 | ): Promise {
12 | const userId = await getUserId()
13 |
14 | const [issue] = await db
15 | .insert(issuesTable)
16 | .values({ ...data, userId })
17 | .returning()
18 | revalidatePath("/")
19 | return issue
20 | }
21 |
22 | export async function getIssuesByProjectId(
23 | projectId: string
24 | ): Promise {
25 | return db.query.issues.findMany({
26 | where: and(eq(issuesTable.projectId, projectId)),
27 | orderBy: desc(issuesTable.createdAt)
28 | })
29 | }
30 |
31 | export async function getIssueById(
32 | id: string
33 | ): Promise {
34 | return db.query.issues.findFirst({
35 | where: eq(issuesTable.id, id)
36 | })
37 | }
38 |
39 | export async function updateIssue(
40 | id: string,
41 | data: Partial
42 | ): Promise {
43 | const [updatedIssue] = await db
44 | .update(issuesTable)
45 | .set(data)
46 | .where(eq(issuesTable.id, id))
47 | .returning()
48 | revalidatePath("/")
49 | return updatedIssue
50 | }
51 |
52 | export async function deleteIssue(id: string): Promise {
53 | await db.delete(issuesTable).where(eq(issuesTable.id, id))
54 | revalidatePath("/")
55 | }
56 |
--------------------------------------------------------------------------------
/app/api/linear/webhook/route.ts:
--------------------------------------------------------------------------------
1 | import { handleWebhook } from "@/actions/linear/webhook"
2 | import { getWorkspaceByLinearOrganizationId } from "@/db/queries"
3 | import { LinearWebhookBody } from "@/types/linear/linear"
4 | import { LinearClient } from "@linear/sdk"
5 | import crypto from "crypto"
6 |
7 | export async function POST(req: Request) {
8 | const body = await req.json()
9 |
10 | if (!isValidSignature(req, body)) {
11 | return new Response("Invalid signature", { status: 400 })
12 | }
13 |
14 | const { actor, organizationId } = body as LinearWebhookBody
15 | const workspace = await getWorkspaceByLinearOrganizationId(organizationId)
16 |
17 | if (!workspace?.linearAccessToken) {
18 | console.error("Profile or Linear access token not found", actor.id)
19 | return new Response("Profile or Linear access token not found", {
20 | status: 400
21 | })
22 | }
23 |
24 | const linearClient = new LinearClient({ apiKey: workspace.linearAccessToken })
25 |
26 | try {
27 | await handleWebhook(linearClient, body)
28 | return new Response("OK", { status: 200 })
29 | } catch (error) {
30 | console.error("Error handling webhook:", error)
31 | return new Response("Internal Server Error", { status: 500 })
32 | }
33 | }
34 |
35 | const isValidSignature = (req: Request, body: any): boolean => {
36 | const signature = crypto
37 | .createHmac("sha256", process.env.LINEAR_WEBHOOK_SECRET!)
38 | .update(JSON.stringify(body))
39 | .digest("hex")
40 |
41 | const linearSignature = req.headers.get("linear-signature")
42 | return signature === linearSignature
43 | }
44 |
--------------------------------------------------------------------------------
/lib/ai/calculate-llm-cost.ts:
--------------------------------------------------------------------------------
1 | export const calculateLLMCost = ({
2 | llmId,
3 | inputTokens,
4 | outputTokens
5 | }: {
6 | llmId: string
7 | inputTokens: number
8 | outputTokens: number
9 | }) => {
10 | const LLMS = [
11 | // Anthropic
12 | {
13 | name: "Claude 3.5 Sonnet",
14 | id: "claude-3-5-sonnet-20240620",
15 | inputCost: 3,
16 | outputCost: 15
17 | },
18 | {
19 | name: "Claude 3.5 Haiku",
20 | id: "claude-3-haiku-20240307",
21 | inputCost: 0.25,
22 | outputCost: 1.25
23 | },
24 | // OpenAI
25 | {
26 | name: "GPT-4 Omni",
27 | id: "gpt-4o",
28 | inputCost: 5,
29 | outputCost: 15
30 | },
31 | {
32 | name: "GPT-4 Turbo",
33 | id: "gpt-4-turbo",
34 | inputCost: 10,
35 | outputCost: 30
36 | },
37 | {
38 | name: "GPT-3.5 Turbo",
39 | id: "gpt-3.5-turbo",
40 | inputCost: 0.5,
41 | outputCost: 1.5
42 | },
43 | // Google
44 | {
45 | name: "Gemini 1.5 Pro",
46 | id: "models/gemini-1.5-pro-latest",
47 | inputCost: 3.5,
48 | outputCost: 10.5
49 | }
50 | ]
51 |
52 | const llm = LLMS.find(llm => llm.id === llmId)
53 | if (!llm) {
54 | return 0 // Skip if LLM not found
55 | }
56 |
57 | const inputCost = (inputTokens / 1_000_000) * llm.inputCost
58 | const outputCost = (outputTokens / 1_000_000) * llm.outputCost
59 | const totalCost = inputCost + outputCost
60 |
61 | // Cost in USD
62 | console.warn(`Total cost for ${llm.name}: $${totalCost.toFixed(6)}`)
63 | return parseFloat(totalCost.toFixed(6))
64 | }
65 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarFallback, AvatarImage }
51 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as TogglePrimitive from "@radix-ui/react-toggle"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "ring-offset-background hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent"
17 | },
18 | size: {
19 | default: "h-10 px-3",
20 | sm: "h-9 px-2.5",
21 | lg: "h-11 px-5"
22 | }
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default"
27 | }
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/lib/ai/parse-ai-response.ts:
--------------------------------------------------------------------------------
1 | import { AIFileInfo, AIParsedResponse } from "@/types/ai"
2 |
3 | export function parseAIResponse(response: string): AIParsedResponse {
4 | const files: AIFileInfo[] = []
5 | const fileListMatch = response.match(/([\s\S]*?)<\/file_list>/)
6 | const fileList = fileListMatch
7 | ? fileListMatch[1]
8 | .trim()
9 | .split("\n")
10 | .map(file => file.trim())
11 | : []
12 |
13 | const fileMatches = response.matchAll(
14 | /[\s\S]*?(.*?)<\/file_path>[\s\S]*?([\s\S]*?)<\/file_content>[\s\S]*?(.*?)<\/file_status>[\s\S]*?<\/file>/g
15 | )
16 |
17 | for (const match of fileMatches) {
18 | const [_, path, language, content, status] = match
19 | files.push({
20 | path: path.trim(),
21 | language: language.trim(),
22 | content: content.trim(),
23 | status: status.trim() as "new" | "modified" | "deleted"
24 | })
25 | }
26 |
27 | // Handle deleted files (which don't have content)
28 | const deletedFileMatches = response.matchAll(
29 | /[\s\S]*?(.*?)<\/file_path>[\s\S]*?deleted<\/file_status>[\s\S]*?<\/file>/g
30 | )
31 |
32 | for (const match of deletedFileMatches) {
33 | const [_, path] = match
34 | files.push({
35 | path: path.trim(),
36 | language: "",
37 | content: "",
38 | status: "deleted"
39 | })
40 | }
41 |
42 | const prTitleMatch = response.match(/([\s\S]*?)<\/pr_title>/)
43 | const prTitle = prTitleMatch ? prTitleMatch[1].trim() : ""
44 |
45 | return { fileList, files, prTitle }
46 | }
47 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
4 | import { Circle } from "lucide-react"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/templates/template-list.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { deleteTemplate } from "@/db/queries/templates-queries"
4 | import { SelectInstruction, SelectTemplate } from "@/db/schema"
5 | import { FC } from "react"
6 | import { DataItem } from "../dashboard/reusable/data-item"
7 | import { DataList } from "../dashboard/reusable/data-list"
8 |
9 | interface TemplatesListProps {
10 | templatesWithInstructions: (SelectTemplate & {
11 | templatesToInstructions: {
12 | templateId: string
13 | instructionId: string
14 | instruction: SelectInstruction
15 | }[]
16 | })[]
17 | instructions: SelectInstruction[]
18 | projectId: string
19 | }
20 |
21 | export const TemplatesList: FC = ({
22 | templatesWithInstructions
23 | }) => {
24 | const handleDeleteTemplate = async (id: string) => {
25 | await deleteTemplate(id)
26 | }
27 | return (
28 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/components/magicui/border-beam.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface BorderBeamProps {
4 | className?: string
5 | size?: number
6 | duration?: number
7 | borderWidth?: number
8 | anchor?: number
9 | colorFrom?: string
10 | colorTo?: string
11 | delay?: number
12 | }
13 |
14 | export const BorderBeam = ({
15 | className,
16 | size = 200,
17 | duration = 15,
18 | anchor = 90,
19 | borderWidth = 1.5,
20 | colorFrom = "#ffaa40",
21 | colorTo = "#9c40ff",
22 | delay = 0
23 | }: BorderBeamProps) => {
24 | return (
25 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/actions/github/embed-branch.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { fetchFiles } from "@/actions/github/fetch-files"
4 | import {
5 | createEmbeddedFiles,
6 | deleteAllEmbeddedFilesByEmbeddedBranchId
7 | } from "@/db/queries/embedded-files-queries"
8 | import { embedFiles } from "./embed-files"
9 | import { fetchCodebaseForBranch } from "./fetch-codebase"
10 | import { tokenizeFiles } from "./tokenize-files"
11 |
12 | export async function embedBranch(data: {
13 | projectId: string
14 | githubRepoFullName: string
15 | branchName: string
16 | embeddedBranchId: string
17 | installationId: number | null
18 | }) {
19 | const {
20 | projectId,
21 | githubRepoFullName,
22 | branchName,
23 | embeddedBranchId,
24 | installationId
25 | } = data
26 |
27 | try {
28 | // clear branch embeddings
29 | await deleteAllEmbeddedFilesByEmbeddedBranchId(embeddedBranchId)
30 |
31 | // fetch codebase for branch
32 | const codebase = await fetchCodebaseForBranch({
33 | githubRepoFullName,
34 | path: "",
35 | branch: branchName,
36 | installationId
37 | })
38 |
39 | // fetch file content
40 | const files = await fetchFiles(installationId, codebase)
41 |
42 | // tokenize files
43 | const tokenizedFiles = await tokenizeFiles(files)
44 |
45 | // embed files
46 | const embeddedFiles = await embedFiles(tokenizedFiles)
47 |
48 | // insert embedded files with data
49 | await createEmbeddedFiles(
50 | embeddedFiles.map(file => ({
51 | ...file,
52 | projectId,
53 | embeddedBranchId,
54 | githubRepoFullName
55 | }))
56 | )
57 | } catch (error) {
58 | console.error("Error in embedBranch:", error)
59 | throw error
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/db/queries/issues-to-instructions-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { and, eq } from "drizzle-orm"
4 | import { revalidatePath } from "next/cache"
5 | import { db } from "../db"
6 | import { issuesToInstructionsTable } from "../schema/issues-to-instructions-schema"
7 |
8 | export async function addInstructionToIssue(
9 | issueId: string,
10 | instructionId: string
11 | ) {
12 | try {
13 | await db
14 | .insert(issuesToInstructionsTable)
15 | .values({ issueId, instructionId })
16 | revalidatePath("/")
17 | } catch (error) {
18 | console.error("Error adding instruction to issue:", error)
19 | throw error
20 | }
21 | }
22 |
23 | export async function removeInstructionFromIssue(
24 | issueId: string,
25 | instructionId: string
26 | ) {
27 | try {
28 | await db
29 | .delete(issuesToInstructionsTable)
30 | .where(
31 | and(
32 | eq(issuesToInstructionsTable.issueId, issueId),
33 | eq(issuesToInstructionsTable.instructionId, instructionId)
34 | )
35 | )
36 | revalidatePath("/")
37 | } catch (error) {
38 | console.error("Error removing instruction from issue:", error)
39 | throw error
40 | }
41 | }
42 |
43 | export async function getInstructionsForIssue(issueId: string) {
44 | try {
45 | return db.query.issuesToInstructions.findMany({
46 | where: eq(issuesToInstructionsTable.issueId, issueId),
47 | with: {
48 | instruction: {
49 | columns: {
50 | id: true,
51 | name: true,
52 | content: true
53 | }
54 | }
55 | }
56 | })
57 | } catch (error) {
58 | console.error("Error getting instructions for issue:", error)
59 | throw error
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/db/queries/templates-to-instructions-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { and, eq } from "drizzle-orm"
4 | import { revalidatePath } from "next/cache"
5 | import { db } from "../db"
6 | import { templatesToInstructionsTable } from "../schema/templates-to-instructions-schema"
7 |
8 | export async function addInstructionToTemplate(
9 | templateId: string,
10 | instructionId: string
11 | ) {
12 | try {
13 | await db
14 | .insert(templatesToInstructionsTable)
15 | .values({ templateId, instructionId })
16 | revalidatePath("/")
17 | } catch (error) {
18 | console.error("Error adding instruction to template:", error)
19 | throw error
20 | }
21 | }
22 |
23 | export async function removeInstructionFromTemplate(
24 | templateId: string,
25 | instructionId: string
26 | ) {
27 | try {
28 | await db
29 | .delete(templatesToInstructionsTable)
30 | .where(
31 | and(
32 | eq(templatesToInstructionsTable.templateId, templateId),
33 | eq(templatesToInstructionsTable.instructionId, instructionId)
34 | )
35 | )
36 | revalidatePath("/")
37 | } catch (error) {
38 | console.error("Error removing instruction from template:", error)
39 | throw error
40 | }
41 | }
42 |
43 | export async function getInstructionsForTemplate(templateId: string) {
44 | try {
45 | return db.query.templatesToInstructions.findMany({
46 | where: eq(templatesToInstructionsTable.templateId, templateId),
47 | with: {
48 | instruction: {
49 | columns: {
50 | id: true,
51 | name: true,
52 | content: true
53 | }
54 | }
55 | }
56 | })
57 | } catch (error) {
58 | console.error("Error getting instructions for template:", error)
59 | throw error
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import { cva, type VariantProps } from "class-variance-authority"
2 | import * as React from "react"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive"
14 | }
15 | },
16 | defaultVariants: {
17 | variant: "default"
18 | }
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertDescription, AlertTitle }
60 |
--------------------------------------------------------------------------------
/app/workspaces/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { createProject } from "@/db/queries/projects-queries"
3 | import {
4 | createWorkspace,
5 | getWorkspacesByUserId
6 | } from "@/db/queries/workspaces-queries"
7 | import Link from "next/link"
8 | import { redirect } from "next/navigation"
9 |
10 | export const revalidate = 0
11 |
12 | export default async function WorkspacesPage() {
13 | const workspaces = await getWorkspacesByUserId()
14 |
15 | const handleCreateWorkspace = async () => {
16 | "use server"
17 | const workspace = await createWorkspace({ name: "My Workspace" })
18 | await createProject({
19 | name: "My Project",
20 | workspaceId: workspace.id
21 | })
22 | return redirect(`/${workspace.id}`)
23 | }
24 |
25 | return (
26 |
27 | {workspaces.length === 0 ? (
28 |
29 |
No workspaces
30 |
33 |
34 | ) : (
35 |
36 |
Select a workspace.
37 | {workspaces.map(workspace => (
38 |
43 |
{workspace.name}
44 |
45 | ))}
46 |
47 | )}
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/templates/edit-template-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { updateTemplate } from "@/db/queries/templates-queries"
4 | import { SelectInstruction, SelectTemplate } from "@/db/schema"
5 | import { useRouter } from "next/navigation"
6 | import { CRUDForm } from "../dashboard/reusable/crud-form"
7 | import { TemplateSelect } from "./template-select"
8 |
9 | export default function EditTemplateForm({
10 | instructions,
11 | templateWithInstructions
12 | }: {
13 | instructions: SelectInstruction[]
14 | templateWithInstructions: SelectTemplate & {
15 | templatesToInstructions: {
16 | templateId: string
17 | instructionId: string
18 | instruction: SelectInstruction
19 | }[]
20 | }
21 | }) {
22 | const router = useRouter()
23 |
24 | const handleUpdateTemplate = async (formData: FormData) => {
25 | try {
26 | const updatedTemplate = {
27 | title: formData.get("title") as string,
28 | content: formData.get("content") as string
29 | }
30 | await updateTemplate(
31 | templateWithInstructions.id,
32 | updatedTemplate,
33 | templateWithInstructions.projectId
34 | )
35 | router.refresh()
36 | router.push(`../${templateWithInstructions.id}`)
37 | } catch (error) {
38 | console.error("Failed to update instruction:", error)
39 | }
40 | }
41 |
42 | return (
43 | <>
44 |
45 |
49 |
50 |
51 |
60 | >
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { GripVertical } from "lucide-react"
4 | import * as ResizablePrimitive from "react-resizable-panels"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | )
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean
29 | }) => (
30 | div]:rotate-90",
33 | className
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | )
44 |
45 | export { ResizableHandle, ResizablePanel, ResizablePanelGroup }
46 |
--------------------------------------------------------------------------------
/components/integrations/connect-linear.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { CircleDot } from "lucide-react"
4 | import { useParams, useRouter } from "next/navigation"
5 | import { FC, useState } from "react"
6 | import { Integration } from "./integration"
7 |
8 | interface ConnectLinearProps {
9 | isLinearConnected: boolean
10 | }
11 |
12 | export const ConnectLinear: FC = ({
13 | isLinearConnected
14 | }) => {
15 | const router = useRouter()
16 | const params = useParams()
17 |
18 | const [isConnecting, setIsConnecting] = useState(false)
19 |
20 | const projectId = params.projectId as string
21 |
22 | const handleConnect = () => {
23 | try {
24 | if (!projectId) {
25 | throw new Error("User ID or project ID not found")
26 | }
27 |
28 | setIsConnecting(true)
29 |
30 | const state = JSON.stringify({ projectId })
31 |
32 | const clientId = process.env.NEXT_PUBLIC_LINEAR_CLIENT_ID
33 | const redirectUri = encodeURIComponent(
34 | `${window.location.origin}/api/auth/callback/linear`
35 | )
36 | const scope = "read,write,issues:create,comments:create"
37 | const actor = "application"
38 |
39 | router.push(
40 | `https://linear.app/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&state=${encodeURIComponent(
41 | state
42 | )}&scope=${scope}&actor=${actor}`
43 | )
44 | } catch (error) {
45 | console.error("Linear connection error:", error)
46 | router.push(`/projects`)
47 | } finally {
48 | setIsConnecting(false)
49 | }
50 | }
51 |
52 | return (
53 | }
56 | isConnecting={isConnecting}
57 | isConnected={isLinearConnected}
58 | disabled={isConnecting || isLinearConnected}
59 | onClick={handleConnect}
60 | />
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/types/linear/linear.ts:
--------------------------------------------------------------------------------
1 | export interface LinearWebhookBody {
2 | action: string
3 | actor: {
4 | id: string
5 | name: string
6 | type: string
7 | }
8 | createdAt: string
9 | data: LinearWebhookIssue | LinearWebhookComment // add more types as needed
10 | url: string
11 | type: string
12 | organizationId: string
13 | webhookTimestamp: number
14 | webhookId: string
15 | }
16 |
17 | export interface LinearWebhookIssue {
18 | id: string
19 | createdAt: string
20 | updatedAt: string
21 | number: number
22 | title: string
23 | priority: number
24 | boardOrder: number
25 | sortOrder: number
26 | labelIds: string[]
27 | teamId: string
28 | previousIdentifiers: string[]
29 | creatorId: string
30 | assigneeId: string
31 | stateId: string
32 | reactionData: {
33 | emoji: string
34 | reactions: {
35 | id: string
36 | userId: string
37 | reactedAt: string
38 | }[]
39 | }[]
40 | priorityLabel: string
41 | botActor: null | any
42 | identifier: string
43 | url: string
44 | assignee: {
45 | id: string
46 | name: string
47 | }
48 | state: {
49 | id: string
50 | color: string
51 | name: string
52 | type: string
53 | }
54 | team: {
55 | id: string
56 | key: string
57 | name: string
58 | }
59 | subscriberIds: string[]
60 | labels: {
61 | id: string
62 | color: string
63 | name: string
64 | }[]
65 | }
66 |
67 | export interface LinearWebhookComment {
68 | id: string
69 | createdAt: string
70 | updatedAt: string
71 | body: string
72 | issueId: string
73 | parentId: string | null
74 | userId: string
75 | reactionData: {
76 | emoji: string
77 | reactions: {
78 | id: string
79 | userId: string
80 | reactedAt: string
81 | }[]
82 | }[]
83 | user: {
84 | id: string
85 | name: string
86 | }
87 | issue: {
88 | id: string
89 | title: string
90 | teamId: string
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/db/schema/embedded-files-schema.ts:
--------------------------------------------------------------------------------
1 | import { BUILDWARE_EMBEDDING_DIMENSIONS } from "@/lib/constants/buildware-config"
2 | import { relations } from "drizzle-orm"
3 | import {
4 | index,
5 | integer,
6 | pgTable,
7 | text,
8 | timestamp,
9 | uuid,
10 | vector
11 | } from "drizzle-orm/pg-core"
12 | import { embeddedBranchesTable } from "./embedded-branches-schema"
13 | import { projectsTable } from "./projects-schema"
14 |
15 | export const embeddedFilesTable = pgTable(
16 | "embedded_files",
17 | {
18 | id: uuid("id").primaryKey().defaultRandom(),
19 | userId: text("user_id").notNull(),
20 | projectId: uuid("project_id")
21 | .notNull()
22 | .references(() => projectsTable.id, { onDelete: "cascade" }),
23 | embeddedBranchId: uuid("embedded_branch_id")
24 | .notNull()
25 | .references(() => embeddedBranchesTable.id, { onDelete: "cascade" }),
26 | githubRepoFullName: text("github_repo_full_name").notNull(),
27 | path: text("path").notNull(),
28 | content: text("content"),
29 | tokenCount: integer("token_count").notNull(),
30 | embedding: vector("embedding", {
31 | dimensions: BUILDWARE_EMBEDDING_DIMENSIONS
32 | }),
33 | createdAt: timestamp("created_at").notNull().defaultNow(),
34 | updatedAt: timestamp("updated_at")
35 | .notNull()
36 | .defaultNow()
37 | .$onUpdate(() => new Date())
38 | },
39 | table => ({
40 | embedding_index: index("embedding_index").using(
41 | "hnsw",
42 | table.embedding.op("vector_cosine_ops")
43 | )
44 | })
45 | )
46 |
47 | export const embeddedFilesRelations = relations(
48 | embeddedFilesTable,
49 | ({ one }) => ({
50 | project: one(projectsTable, {
51 | fields: [embeddedFilesTable.projectId],
52 | references: [projectsTable.id]
53 | })
54 | })
55 | )
56 |
57 | export type InsertEmbeddedFile = typeof embeddedFilesTable.$inferInsert
58 | export type SelectEmbeddedFile = typeof embeddedFilesTable.$inferSelect
59 |
--------------------------------------------------------------------------------
/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
4 | import { VariantProps } from "class-variance-authority"
5 | import * as React from "react"
6 |
7 | import { toggleVariants } from "@/components/ui/toggle"
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default"
15 | })
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ))
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext)
41 |
42 | return (
43 |
54 | {children}
55 |
56 | )
57 | })
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/components/templates/new-template-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { createTemplateRecords } from "@/db/queries/templates-queries"
4 | import { addInstructionToTemplate } from "@/db/queries/templates-to-instructions-queries"
5 | import { SelectInstruction } from "@/db/schema"
6 | import { useParams, useRouter } from "next/navigation"
7 | import { useState } from "react"
8 | import { CRUDForm } from "../dashboard/reusable/crud-form"
9 | import { MultiSelect } from "../ui/multi-select"
10 |
11 | interface NewTemplateFormProps {
12 | instructions: SelectInstruction[]
13 | }
14 |
15 | export default function NewTemplateForm({
16 | instructions
17 | }: NewTemplateFormProps) {
18 | const params = useParams()
19 | const router = useRouter()
20 | const [selectedInstructions, setSelectedInstructions] = useState([])
21 |
22 | const projectId = params.projectId as string
23 |
24 | const handleCreateTemplate = async (formData: FormData) => {
25 | const newTemplate = {
26 | name: formData.get("name") as string,
27 | content: formData.get("content") as string,
28 | projectId
29 | }
30 | const template = await createTemplateRecords([newTemplate])
31 |
32 | // Add selected instructions to the template
33 | for (const instructionId of selectedInstructions) {
34 | await addInstructionToTemplate(template[0].id, instructionId)
35 | }
36 |
37 | router.refresh()
38 | router.push(`../templates/${template[0].id}`)
39 | }
40 |
41 | return (
42 | <>
43 |
44 | ({
47 | id: instruction.id,
48 | name: instruction.name
49 | }))}
50 | selectedIds={selectedInstructions}
51 | onToggleSelect={setSelectedInstructions}
52 | />
53 |
54 |
59 | >
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/db/db.ts:
--------------------------------------------------------------------------------
1 | import { neon } from "@neondatabase/serverless"
2 | import { config } from "dotenv"
3 | import { drizzle } from "drizzle-orm/neon-http"
4 | import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js"
5 | import postgres from "postgres"
6 | import * as schema from "./schema"
7 |
8 | config({ path: ".env.local" })
9 |
10 | const databaseUrl = process.env.DATABASE_URL
11 |
12 | if (!databaseUrl) {
13 | throw new Error("DATABASE_URL is not set")
14 | }
15 |
16 | const dbSchema = {
17 | // Tables
18 | profiles: schema.profilesTable,
19 | projects: schema.projectsTable,
20 | issues: schema.issuesTable,
21 | templates: schema.templatesTable,
22 | instructions: schema.instructionsTable,
23 | templatesToInstructions: schema.templatesToInstructionsTable,
24 | embeddedFiles: schema.embeddedFilesTable,
25 | embeddedBranches: schema.embeddedBranchesTable,
26 | issuesToInstructions: schema.issuesToInstructionsTable,
27 | issueMessages: schema.issueMessagesTable,
28 | workspaces: schema.workspacesTable,
29 |
30 | // Relations
31 | projectsRelations: schema.projectsRelations,
32 | issuesRelations: schema.issuesRelations,
33 | templateRelations: schema.templateRelations,
34 | instructionsRelations: schema.instructionsRelations,
35 | templateToInstructionsRelations: schema.templateToInstructionsRelations,
36 | embeddedFilesRelations: schema.embeddedFilesRelations,
37 | embeddedBranchesRelations: schema.embeddedBranchesRelations,
38 | issuesToInstructionsRelations: schema.issueToInstructionsRelations,
39 | issueMessagesRelations: schema.issueMessagesRelations,
40 | workspacesRelations: schema.workspacesRelations
41 | }
42 |
43 | function initializeDb(url: string) {
44 | const isNeon = url.includes("neon")
45 |
46 | if (isNeon) {
47 | const client = neon(url)
48 | return drizzle(client, { schema: dbSchema })
49 | } else {
50 | const client = postgres(url, { prepare: false })
51 | return drizzlePostgres(client, { schema: dbSchema })
52 | }
53 | }
54 |
55 | export const db = initializeDb(databaseUrl)
56 |
--------------------------------------------------------------------------------
/actions/github/embed-target-branch.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { embedBranch } from "@/actions/github/embed-branch"
4 | import {
5 | createEmbeddedBranch,
6 | findEmbeddedBranch,
7 | updateEmbeddedBranchById
8 | } from "@/db/queries/embedded-branches-queries"
9 | import { getAuthenticatedOctokit } from "./auth"
10 |
11 | interface EmbedTargetBranchParams {
12 | projectId: string
13 | githubRepoFullName: string
14 | branchName: string
15 | installationId: number | null
16 | }
17 |
18 | export async function embedTargetBranch({
19 | projectId,
20 | githubRepoFullName,
21 | branchName,
22 | installationId
23 | }: EmbedTargetBranchParams) {
24 | try {
25 | const [owner, repo] = githubRepoFullName.split("/")
26 | const octokit = await getAuthenticatedOctokit(installationId)
27 |
28 | // Fetch the latest commit hash
29 | const { data: branchData } = await octokit.repos.getBranch({
30 | owner,
31 | repo,
32 | branch: branchName
33 | })
34 | const latestCommitHash = branchData.commit.sha
35 |
36 | let embeddedBranch = await findEmbeddedBranch({
37 | projectId,
38 | githubRepoFullName,
39 | branchName
40 | })
41 |
42 | if (!embeddedBranch) {
43 | embeddedBranch = await createEmbeddedBranch({
44 | projectId,
45 | githubRepoFullName,
46 | branchName,
47 | lastEmbeddedCommitHash: null
48 | })
49 | }
50 |
51 | // Check if the branch needs updating
52 | if (embeddedBranch.lastEmbeddedCommitHash !== latestCommitHash) {
53 | console.warn("Branch needs updating")
54 | await embedBranch({
55 | projectId,
56 | githubRepoFullName,
57 | branchName,
58 | embeddedBranchId: embeddedBranch.id,
59 | installationId
60 | })
61 |
62 | await updateEmbeddedBranchById(embeddedBranch.id, {
63 | lastEmbeddedCommitHash: latestCommitHash
64 | })
65 | }
66 |
67 | return embeddedBranch
68 | } catch (error) {
69 | console.error("Error embedding target branch:", error)
70 | throw error
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as TabsPrimitive from "@radix-ui/react-tabs"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsContent, TabsList, TabsTrigger }
56 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border-input bg-background hover:bg-accent hover:text-accent-foreground border",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | create: "bg-create text-create-foreground hover:bg-create/90"
22 | },
23 | size: {
24 | default: "h-10 px-4 py-2",
25 | sm: "h-9 rounded-md px-3",
26 | lg: "h-11 rounded-md px-8",
27 | icon: "size-10"
28 | }
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default"
33 | }
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 0 0% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 0 0% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 0 0% 3.9%;
15 |
16 | --primary: 0 0% 9%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 0 0% 96.1%;
20 | --secondary-foreground: 0 0% 9%;
21 |
22 | --muted: 0 0% 96.1%;
23 | --muted-foreground: 0 0% 45.1%;
24 |
25 | --accent: 0 0% 96.1%;
26 | --accent-foreground: 0 0% 9%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --create: 220 100% 50%;
32 | --create-foreground: 0 0% 98%;
33 |
34 | --border: 0 0% 89.8%;
35 | --input: 0 0% 89.8%;
36 | --ring: 0 0% 3.9%;
37 |
38 | --radius: 0.5rem;
39 |
40 | /* Custom properties */
41 | --navigation-height: 3.5rem;
42 | --color-one: #1133b0;
43 | --color-two: #fe8bbb;
44 | --color-three: #9e7aff;
45 |
46 | /* --color-one: #37ecba;
47 | --color-two: #72afd3;
48 | --color-three: #ff2e63; */
49 | }
50 |
51 | .dark {
52 | --background: 0 0% 0%;
53 | --foreground: 0 0% 98%;
54 |
55 | --card: 0 0% 3.9%;
56 | --card-foreground: 0 0% 98%;
57 |
58 | --popover: 0 0% 3.9%;
59 | --popover-foreground: 0 0% 98%;
60 |
61 | --primary: 0 0% 98%;
62 | --primary-foreground: 0 0% 9%;
63 |
64 | --secondary: 0 0% 14.9%;
65 | --secondary-foreground: 0 0% 98%;
66 |
67 | --muted: 0 0% 14.9%;
68 | --muted-foreground: 0 0% 63.9%;
69 |
70 | --accent: 0 0% 14.9%;
71 | --accent-foreground: 0 0% 98%;
72 |
73 | --destructive: 0 62.8% 30.6%;
74 | --destructive-foreground: 0 0% 98%;
75 |
76 | --create: 220 80% 50%;
77 | --create-foreground: 0 0% 98%;
78 |
79 | --border: 0 0% 14.9%;
80 | --input: 0 0% 14.9%;
81 | --ring: 0 0% 83.1%;
82 | }
83 | }
84 |
85 | @layer base {
86 | * {
87 | @apply border-border;
88 | }
89 | body {
90 | @apply bg-background text-foreground;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/components/workspaces/create-workspace-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import {
5 | Dialog,
6 | DialogContent,
7 | DialogFooter,
8 | DialogHeader,
9 | DialogTitle,
10 | DialogTrigger
11 | } from "@/components/ui/dialog"
12 | import { Input } from "@/components/ui/input"
13 | import { createWorkspace } from "@/db/queries/workspaces-queries"
14 | import { cn } from "@/lib/utils"
15 | import { PlusIcon } from "lucide-react"
16 | import { useRouter } from "next/navigation"
17 | import { FC, HTMLAttributes, useState } from "react"
18 |
19 | interface CreateWorkspaceButtonProps extends HTMLAttributes {}
20 |
21 | export const CreateWorkspaceButton: FC = ({
22 | ...props
23 | }) => {
24 | const router = useRouter()
25 | const [open, setOpen] = useState(false)
26 | const [workspaceName, setWorkspaceName] = useState("")
27 |
28 | const handleCreateWorkspace = async () => {
29 | try {
30 | const workspace = await createWorkspace({
31 | name: workspaceName || "My Workspace"
32 | })
33 | router.push(`/${workspace.id}`)
34 | setOpen(false)
35 | } catch (error) {
36 | console.error(error)
37 | }
38 | }
39 |
40 | return (
41 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
80 |
--------------------------------------------------------------------------------
/actions/github/fetch-files.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { GitHubFile, GitHubFileContent } from "@/types/github"
4 | import { getAuthenticatedOctokit } from "./auth"
5 | import { fetchWithRetry } from "./fetch-codebase"
6 |
7 | export async function fetchFiles(
8 | installationId: number | null,
9 | files: GitHubFile[]
10 | ) {
11 | // List of file extensions to exclude
12 | const excludedExtensions = [
13 | ".jpg",
14 | ".jpeg",
15 | ".png",
16 | ".gif",
17 | ".bmp",
18 | ".svg",
19 | ".tiff",
20 | ".webp",
21 | ".ico",
22 | ".heic",
23 | ".raw"
24 | ]
25 |
26 | // List of files to exclude
27 | const excludedFiles = ["package-lock.json"]
28 |
29 | // List of directories to exclude
30 | const excludedDirs = ["public", "migrations", "node_modules"]
31 |
32 | // Filter out unwanted files or directories based on excludedFiles, excludedDirs, and excludedExtensions
33 | const filteredFiles = files.filter(
34 | (file: any) =>
35 | !excludedFiles.includes(file.name) &&
36 | !excludedDirs.some(dir => file.path.includes(dir)) &&
37 | !excludedExtensions.some(extension => file.name.endsWith(extension))
38 | )
39 |
40 | const octokit = await getAuthenticatedOctokit(installationId)
41 |
42 | // Fetch the content of each file using Octokit with retry logic
43 | const fetchPromises = filteredFiles.map(async (file: GitHubFile) => {
44 | try {
45 | const { data } = await fetchWithRetry(octokit, {
46 | owner: file.owner,
47 | repo: file.repo,
48 | path: file.path,
49 | ref: file.ref
50 | })
51 |
52 | if (Array.isArray(data) || !("content" in data)) {
53 | throw new Error(`Unexpected response for ${file.path}`)
54 | }
55 |
56 | return {
57 | name: file.name,
58 | path: file.path,
59 | content: Buffer.from(data.content, "base64").toString("utf-8")
60 | }
61 | } catch (error) {
62 | console.error("Error fetching file:", file, error)
63 | throw error
64 | }
65 | })
66 |
67 | // Wait for all fetch promises to resolve
68 | const filesContent = await Promise.all(fetchPromises)
69 |
70 | return filesContent as GitHubFileContent[]
71 | }
72 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
4 | import { ChevronDown } from "lucide-react"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ))
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
57 |
58 | export { Accordion, AccordionContent, AccordionItem, AccordionTrigger }
59 |
--------------------------------------------------------------------------------
/db/queries/instructions-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getUserId } from "@/actions/auth/auth"
4 | import { eq } from "drizzle-orm"
5 | import { revalidatePath } from "next/cache"
6 | import { db } from "../db"
7 | import {
8 | InsertInstruction,
9 | SelectInstruction,
10 | instructionsTable
11 | } from "../schema/instructions-schema"
12 |
13 | export async function createInstructionRecords(
14 | data: Omit[]
15 | ): Promise {
16 | const userId = await getUserId()
17 |
18 | try {
19 | const result = await db
20 | .insert(instructionsTable)
21 | .values(data.map(instruction => ({ ...instruction, userId })))
22 | .returning()
23 | revalidatePath("/")
24 | return result
25 | } catch (error) {
26 | console.error("Error creating instruction records:", error)
27 | throw error
28 | }
29 | }
30 |
31 | export async function getInstructionById(
32 | id: string
33 | ): Promise {
34 | try {
35 | const result = await db.query.instructions.findFirst({
36 | where: eq(instructionsTable.id, id)
37 | })
38 | return result
39 | } catch (error) {
40 | console.error(`Error getting instruction by id ${id}:`, error)
41 | throw error
42 | }
43 | }
44 |
45 | export async function getInstructionsByProjectId(
46 | projectId: string
47 | ): Promise {
48 | return db.query.instructions.findMany({
49 | where: eq(instructionsTable.projectId, projectId)
50 | })
51 | }
52 |
53 | export async function updateInstruction(
54 | id: string,
55 | data: Partial
56 | ): Promise {
57 | try {
58 | await db
59 | .update(instructionsTable)
60 | .set(data)
61 | .where(eq(instructionsTable.id, id))
62 | revalidatePath("/")
63 | } catch (error) {
64 | console.error(`Error updating instruction ${id}:`, error)
65 | throw error
66 | }
67 | }
68 |
69 | export async function deleteInstruction(id: string): Promise {
70 | try {
71 | await db.delete(instructionsTable).where(eq(instructionsTable.id, id))
72 | revalidatePath("/")
73 | } catch (error) {
74 | console.error(`Error deleting instruction ${id}:`, error)
75 | throw error
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/api/auth/callback/linear/route.ts:
--------------------------------------------------------------------------------
1 | import { updateWorkspace } from "@/db/queries"
2 | import { LinearClient } from "@linear/sdk"
3 | import { revalidatePath } from "next/cache"
4 | import { headers } from "next/headers"
5 | import { redirect } from "next/navigation"
6 | import { NextRequest } from "next/server"
7 |
8 | export async function GET(req: NextRequest) {
9 | const { searchParams } = new URL(req.url)
10 | const code = searchParams.get("code")
11 | const state = searchParams.get("state")
12 |
13 | if (!code || !state) {
14 | return new Response(JSON.stringify({ error: "Invalid OAuth callback" }), {
15 | status: 400,
16 | headers: { "Content-Type": "application/json" }
17 | })
18 | }
19 |
20 | const headersList = headers()
21 | const host = headersList.get("host") || ""
22 | const protocol = process.env.NODE_ENV === "development" ? "http" : "https"
23 | const origin = `${protocol}://${host}`
24 |
25 | const { workspaceId } = JSON.parse(decodeURIComponent(state))
26 |
27 | try {
28 | const tokenResponse = await fetch("https://api.linear.app/oauth/token", {
29 | method: "POST",
30 | headers: { "Content-Type": "application/x-www-form-urlencoded" },
31 | body: new URLSearchParams({
32 | code,
33 | redirect_uri: `${origin}/api/auth/callback/linear`,
34 | client_id: process.env.NEXT_PUBLIC_LINEAR_CLIENT_ID!,
35 | client_secret: process.env.LINEAR_CLIENT_SECRET!,
36 | grant_type: "authorization_code"
37 | })
38 | })
39 |
40 | if (!tokenResponse.ok) {
41 | const errorData = await tokenResponse.text()
42 | console.error("Linear token exchange error:", errorData)
43 | throw new Error(
44 | `Failed to exchange code for token: ${tokenResponse.status} ${tokenResponse.statusText}`
45 | )
46 | }
47 |
48 | const { access_token } = await tokenResponse.json()
49 |
50 | const linearClient = new LinearClient({
51 | apiKey: access_token
52 | })
53 |
54 | const organization = await linearClient.organization
55 |
56 | await updateWorkspace(workspaceId, {
57 | linearOrganizationId: organization.id,
58 | linearAccessToken: access_token
59 | })
60 |
61 | revalidatePath(`/`)
62 | } catch (error) {
63 | console.error("Error during Linear OAuth:", error)
64 | }
65 |
66 | return redirect(`/${workspaceId}/settings`)
67 | }
68 |
--------------------------------------------------------------------------------
/components/ui/input-otp.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { OTPInput, OTPInputContext } from "input-otp"
4 | import { Dot } from "lucide-react"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const InputOTP = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, containerClassName, ...props }, ref) => (
13 |
22 | ))
23 | InputOTP.displayName = "InputOTP"
24 |
25 | const InputOTPGroup = React.forwardRef<
26 | React.ElementRef<"div">,
27 | React.ComponentPropsWithoutRef<"div">
28 | >(({ className, ...props }, ref) => (
29 |
30 | ))
31 | InputOTPGroup.displayName = "InputOTPGroup"
32 |
33 | const InputOTPSlot = React.forwardRef<
34 | React.ElementRef<"div">,
35 | React.ComponentPropsWithoutRef<"div"> & { index: number }
36 | >(({ index, className, ...props }, ref) => {
37 | const inputOTPContext = React.useContext(OTPInputContext)
38 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
39 |
40 | return (
41 |
50 | {char}
51 | {hasFakeCaret && (
52 |
55 | )}
56 |
57 | )
58 | })
59 | InputOTPSlot.displayName = "InputOTPSlot"
60 |
61 | const InputOTPSeparator = React.forwardRef<
62 | React.ElementRef<"div">,
63 | React.ComponentPropsWithoutRef<"div">
64 | >(({ ...props }, ref) => (
65 |
66 |
67 |
68 | ))
69 | InputOTPSeparator.displayName = "InputOTPSeparator"
70 |
71 | export { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot }
72 |
--------------------------------------------------------------------------------
/actions/retrieval/get-similar-files.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { generateEmbedding } from "@/actions/ai/generate-embedding"
4 | import { db } from "@/db/db"
5 | import { getProjectById } from "@/db/queries"
6 | import { embeddedBranchesTable } from "@/db/schema"
7 | import { embeddedFilesTable } from "@/db/schema/embedded-files-schema"
8 | import { BUILDWARE_MAX_INPUT_TOKENS } from "@/lib/constants/buildware-config"
9 | import { and, cosineDistance, desc, eq, gt, sql } from "drizzle-orm"
10 |
11 | export const getMostSimilarEmbeddedFiles = async (
12 | text: string,
13 | projectId: string
14 | ) => {
15 | const project = await getProjectById(projectId)
16 | if (!project) {
17 | throw new Error("Project not found")
18 | }
19 |
20 | const embeddedBranch = await db.query.embeddedBranches.findFirst({
21 | where: and(
22 | eq(embeddedBranchesTable.projectId, projectId),
23 | eq(embeddedBranchesTable.branchName, project.githubTargetBranch || "")
24 | )
25 | })
26 | if (!embeddedBranch) {
27 | throw new Error("Embedded branch not found")
28 | }
29 |
30 | const embedding = await generateEmbedding(text)
31 |
32 | const similarity = sql`1 - (${cosineDistance(embeddedFilesTable.embedding, embedding)})`
33 |
34 | const mostSimilarEmbeddedFiles = await db
35 | .select({
36 | id: embeddedFilesTable.id,
37 | projectId: embeddedFilesTable.projectId,
38 | embeddedBranchId: embeddedFilesTable.embeddedBranchId,
39 | githubRepoFullName: embeddedFilesTable.githubRepoFullName,
40 | path: embeddedFilesTable.path,
41 | content: embeddedFilesTable.content,
42 | tokenCount: embeddedFilesTable.tokenCount,
43 | createdAt: embeddedFilesTable.createdAt,
44 | updatedAt: embeddedFilesTable.updatedAt,
45 | similarity
46 | })
47 | .from(embeddedFilesTable)
48 | .where(
49 | and(
50 | gt(similarity, 0.01),
51 | eq(embeddedFilesTable.embeddedBranchId, embeddedBranch.id)
52 | )
53 | )
54 | .orderBy(t => desc(t.similarity))
55 |
56 | const filteredFiles = mostSimilarEmbeddedFiles.reduce(
57 | (acc: { files: typeof mostSimilarEmbeddedFiles; tokens: number }, file) => {
58 | if (acc.tokens + file.tokenCount <= BUILDWARE_MAX_INPUT_TOKENS) {
59 | acc.files.push(file)
60 | acc.tokens += file.tokenCount
61 | }
62 | return acc
63 | },
64 | { files: [], tokens: 0 }
65 | ).files
66 |
67 | return filteredFiles
68 | }
69 |
--------------------------------------------------------------------------------
/components/dashboard/reusable/data-item.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { MoreHorizontal, Pencil, Trash } from "lucide-react"
4 | import Link from "next/link"
5 | import { FC } from "react"
6 | import { Button } from "../../ui/button"
7 | import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"
8 | import { Separator } from "../../ui/separator"
9 |
10 | interface DataItemProps {
11 | data: {
12 | id: string
13 | name: string
14 | }
15 | type: "instructions" | "templates" | "issues"
16 | onDelete: (id: string) => Promise
17 | }
18 |
19 | export const DataItem: FC = ({ data, type, onDelete }) => {
20 | return (
21 |
22 |
26 |
{data.name}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
49 |
50 |
51 |
59 |
60 |
61 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/components/instructions/message-markdown.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import React, { FC, HTMLAttributes } from "react"
3 | import remarkGfm from "remark-gfm"
4 | import remarkMath from "remark-math"
5 | import { MessageCodeBlock } from "./message-codeblock"
6 | import { MessageMarkdownMemoized } from "./message-markdown-memoized"
7 |
8 | interface MessageMarkdownProps extends HTMLAttributes {
9 | content: string
10 | }
11 |
12 | export const MessageMarkdown: FC = ({
13 | content,
14 | ...props
15 | }) => {
16 | return (
17 | {children}
26 | },
27 | img({ ...props }) {
28 | return
29 | },
30 | code({ className, children, ...props }) {
31 | const childArray = React.Children.toArray(children)
32 | const firstChild = childArray[0] as React.ReactElement
33 | const firstChildAsString = React.isValidElement(firstChild)
34 | ? (firstChild as React.ReactElement).props.children
35 | : firstChild
36 |
37 | if (firstChildAsString === "▍") {
38 | return ▍
39 | }
40 |
41 | if (typeof firstChildAsString === "string") {
42 | childArray[0] = firstChildAsString.replace("`▍`", "▍")
43 | }
44 |
45 | const match = /language-(\w+)/.exec(className || "")
46 |
47 | if (
48 | typeof firstChildAsString === "string" &&
49 | !firstChildAsString.includes("\n")
50 | ) {
51 | return (
52 |
53 | {childArray}
54 |
55 | )
56 | }
57 |
58 | return (
59 |
65 | )
66 | }
67 | }}
68 | >
69 | {content}
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/components/magicui/shine-border.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | type TColorProp = string | string[];
6 |
7 | interface ShineBorderProps {
8 | borderRadius?: number;
9 | borderWidth?: number;
10 | duration?: number;
11 | color?: TColorProp;
12 | className?: string;
13 | children: React.ReactNode;
14 | }
15 |
16 | /**
17 | * @name Shine Border
18 | * @description It is an animated background border effect component with easy to use and configurable props.
19 | * @param borderRadius defines the radius of the border.
20 | * @param borderWidth defines the width of the border.
21 | * @param duration defines the animation duration to be applied on the shining border
22 | * @param color a string or string array to define border color.
23 | * @param className defines the class name to be applied to the component
24 | * @param children contains react node elements.
25 | */
26 | export default function ShineBorder({
27 | borderRadius = 8,
28 | borderWidth = 1,
29 | duration = 14,
30 | color = "#000000",
31 | className,
32 | children,
33 | }: ShineBorderProps) {
34 | return (
35 |
46 |
58 | {children}
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/components/projects/delete-project-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger
13 | } from "@/components/ui/alert-dialog"
14 | import { Button } from "@/components/ui/button"
15 | import { deleteProject } from "@/db/queries/projects-queries"
16 | import { cn } from "@/lib/utils"
17 | import { Trash2 } from "lucide-react"
18 | import { useRouter } from "next/navigation"
19 | import { HTMLAttributes, useState } from "react"
20 | import { toast } from "sonner"
21 |
22 | interface DeleteProjectButtonProps extends HTMLAttributes {
23 | projectId: string
24 | workspaceId: string
25 | }
26 |
27 | export function DeleteProjectButton({
28 | projectId,
29 | workspaceId,
30 | ...props
31 | }: DeleteProjectButtonProps) {
32 | const router = useRouter()
33 | const [isDeleting, setIsDeleting] = useState(false)
34 |
35 | const handleDeleteProject = async () => {
36 | setIsDeleting(true)
37 | try {
38 | await deleteProject(projectId)
39 | toast.success("Project deleted successfully")
40 | router.push(`/${workspaceId}`)
41 | } catch (error) {
42 | console.error("Failed to delete project:", error)
43 | toast.error("Failed to delete project. Please try again.")
44 | } finally {
45 | setIsDeleting(false)
46 | }
47 | }
48 |
49 | return (
50 |
51 |
52 |
56 |
57 |
58 |
59 |
60 |
61 | Are you sure you want to delete this project?
62 |
63 |
64 | This action cannot be undone. This will permanently delete the
65 | project and all associated data.
66 |
67 |
68 |
69 | Cancel
70 |
74 | {isDeleting ? "Deleting..." : "Delete"}
75 |
76 |
77 |
78 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/db/queries/workspaces-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getUserId } from "@/actions/auth/auth"
4 | import { and, desc, eq } from "drizzle-orm"
5 | import { revalidatePath } from "next/cache"
6 | import { db } from "../db"
7 | import {
8 | InsertWorkspace,
9 | SelectWorkspace,
10 | workspacesTable
11 | } from "../schema/workspaces-schema"
12 |
13 | export async function createWorkspace(
14 | data: Omit
15 | ): Promise {
16 | const userId = await getUserId()
17 |
18 | try {
19 | const [result] = await db
20 | .insert(workspacesTable)
21 | .values({ ...data, userId })
22 | .returning()
23 | revalidatePath("/")
24 | return result
25 | } catch (error) {
26 | console.error("Error creating workspace record:", error)
27 | throw error
28 | }
29 | }
30 |
31 | export async function getWorkspaceById(
32 | id: string
33 | ): Promise {
34 | try {
35 | return await db.query.workspaces.findFirst({
36 | where: eq(workspacesTable.id, id)
37 | })
38 | } catch (error) {
39 | console.error(`Error getting workspace by id ${id}:`, error)
40 | throw error
41 | }
42 | }
43 |
44 | export async function getWorkspacesByUserId(): Promise {
45 | const userId = await getUserId()
46 |
47 | try {
48 | return await db.query.workspaces.findMany({
49 | where: eq(workspacesTable.userId, userId),
50 | orderBy: desc(workspacesTable.createdAt)
51 | })
52 | } catch (error) {
53 | console.error("Error getting all workspaces:", error)
54 | throw error
55 | }
56 | }
57 |
58 | export async function getWorkspaceByLinearOrganizationId(
59 | linearOrganizationId: string
60 | ): Promise {
61 | return db.query.workspaces.findFirst({
62 | where: eq(workspacesTable.linearOrganizationId, linearOrganizationId)
63 | })
64 | }
65 |
66 | export async function updateWorkspace(
67 | id: string,
68 | data: Partial
69 | ): Promise {
70 | try {
71 | await db
72 | .update(workspacesTable)
73 | .set(data)
74 | .where(and(eq(workspacesTable.id, id)))
75 | revalidatePath("/")
76 | } catch (error) {
77 | console.error(`Error updating workspace ${id}:`, error)
78 | throw error
79 | }
80 | }
81 |
82 | export async function deleteWorkspace(id: string): Promise {
83 | try {
84 | await db.delete(workspacesTable).where(and(eq(workspacesTable.id, id)))
85 | revalidatePath("/")
86 | } catch (error) {
87 | console.error(`Error deleting workspace ${id}:`, error)
88 | throw error
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/db/queries/embedded-branches-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getUserId } from "@/actions/auth/auth"
4 | import { and, eq } from "drizzle-orm"
5 | import { revalidatePath } from "next/cache"
6 | import { db } from "../db"
7 | import {
8 | InsertEmbeddedBranch,
9 | SelectEmbeddedBranch,
10 | embeddedBranchesTable
11 | } from "../schema"
12 |
13 | export async function createEmbeddedBranch(
14 | data: Omit
15 | ): Promise {
16 | const userId = await getUserId()
17 |
18 | try {
19 | const result = await db
20 | .insert(embeddedBranchesTable)
21 | .values({ ...data, userId })
22 | .returning()
23 | revalidatePath("/")
24 | return result[0]
25 | } catch (error) {
26 | console.error("Error creating embedded branch records:", error)
27 | throw error
28 | }
29 | }
30 |
31 | export async function getEmbeddedBranchesByProjectId(projectId: string) {
32 | try {
33 | const branches = await db.query.embeddedBranches.findMany({
34 | where: and(eq(embeddedBranchesTable.projectId, projectId))
35 | })
36 | return branches
37 | } catch (error) {
38 | console.error(error)
39 | }
40 | }
41 |
42 | export async function getEmbeddedBranchById(id: string) {
43 | try {
44 | const branch = await db.query.embeddedBranches.findFirst({
45 | where: and(eq(embeddedBranchesTable.id, id))
46 | })
47 | return branch
48 | } catch (error) {
49 | console.error("Error fetching embedded branch:", error)
50 | throw error
51 | }
52 | }
53 |
54 | export async function updateEmbeddedBranchById(
55 | id: string,
56 | data: Partial
57 | ) {
58 | try {
59 | const result = await db
60 | .update(embeddedBranchesTable)
61 | .set(data)
62 | .where(eq(embeddedBranchesTable.id, id))
63 | .returning()
64 | revalidatePath("/")
65 | return result[0]
66 | } catch (error) {
67 | console.error("Error updating embedded branch:", error)
68 | throw error
69 | }
70 | }
71 |
72 | export async function findEmbeddedBranch({
73 | projectId,
74 | githubRepoFullName,
75 | branchName
76 | }: {
77 | projectId: string
78 | githubRepoFullName: string
79 | branchName: string
80 | }) {
81 | try {
82 | const branch = await db.query.embeddedBranches.findFirst({
83 | where: and(
84 | eq(embeddedBranchesTable.projectId, projectId),
85 | eq(embeddedBranchesTable.githubRepoFullName, githubRepoFullName),
86 | eq(embeddedBranchesTable.branchName, branchName)
87 | )
88 | })
89 | return branch
90 | } catch (error) {
91 | console.error("Error finding embedded branch:", error)
92 | throw error
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/db/queries/issue-messages-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { and, desc, eq } from "drizzle-orm"
4 | import { revalidatePath } from "next/cache"
5 | import { db } from "../db"
6 | import {
7 | InsertIssueMessage,
8 | SelectIssueMessage,
9 | issueMessagesTable
10 | } from "../schema/issue-messages-schema"
11 |
12 | export async function createIssueMessageRecord(
13 | data: InsertIssueMessage
14 | ): Promise {
15 | try {
16 | const [result] = await db
17 | .insert(issueMessagesTable)
18 | .values({ ...data })
19 | .returning()
20 | revalidatePath("/")
21 | return result
22 | } catch (error) {
23 | console.error("Error creating issue message record:", error)
24 | throw error
25 | }
26 | }
27 |
28 | export async function getIssueMessageById(
29 | id: string
30 | ): Promise {
31 | try {
32 | return db.query.issueMessages.findFirst({
33 | where: eq(issueMessagesTable.id, id)
34 | })
35 | } catch (error) {
36 | console.error(`Error getting issue message by id ${id}:`, error)
37 | throw error
38 | }
39 | }
40 |
41 | export async function getIssueMessagesByIssueId(
42 | issueId: string
43 | ): Promise {
44 | try {
45 | return db.query.issueMessages.findMany({
46 | where: eq(issueMessagesTable.issueId, issueId),
47 | orderBy: desc(issueMessagesTable.createdAt)
48 | })
49 | } catch (error) {
50 | console.error(`Error getting issue messages for issue ${issueId}:`, error)
51 | throw error
52 | }
53 | }
54 |
55 | export async function updateIssueMessage(
56 | id: string,
57 | data: Partial
58 | ): Promise {
59 | try {
60 | await db
61 | .update(issueMessagesTable)
62 | .set(data)
63 | .where(and(eq(issueMessagesTable.id, id)))
64 | revalidatePath("/")
65 | } catch (error) {
66 | console.error(`Error updating issue message ${id}:`, error)
67 | throw error
68 | }
69 | }
70 |
71 | export async function deleteIssueMessage(id: string): Promise {
72 | try {
73 | await db
74 | .delete(issueMessagesTable)
75 | .where(and(eq(issueMessagesTable.id, id)))
76 | revalidatePath("/")
77 | } catch (error) {
78 | console.error(`Error deleting issue message ${id}:`, error)
79 | throw error
80 | }
81 | }
82 |
83 | export async function deleteIssueMessagesByIssueId(
84 | issueId: string
85 | ): Promise {
86 | try {
87 | await db
88 | .delete(issueMessagesTable)
89 | .where(eq(issueMessagesTable.issueId, issueId))
90 | revalidatePath("/")
91 | } catch (error) {
92 | console.error(`Error deleting issue messages for issue ${issueId}:`, error)
93 | throw error
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/actions/github/fetch-codebase.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { GitHubFile } from "@/types/github"
4 | import { Octokit } from "@octokit/rest"
5 | import { getAuthenticatedOctokit } from "./auth"
6 |
7 | const MAX_RETRIES = 5
8 | const INITIAL_DELAY = 1000 // 1 second
9 |
10 | export async function fetchWithRetry(
11 | octokit: Octokit,
12 | params: any,
13 | retries = 0
14 | ): Promise {
15 | try {
16 | return await octokit.repos.getContent(params)
17 | } catch (error: any) {
18 | if (
19 | error.status === 403 &&
20 | error.message.includes("secondary rate limit") &&
21 | retries < MAX_RETRIES
22 | ) {
23 | const delay = INITIAL_DELAY * Math.pow(2, retries)
24 | console.warn(`Hit secondary rate limit. Retrying in ${delay}ms...`)
25 | await new Promise(resolve => setTimeout(resolve, delay))
26 | return fetchWithRetry(octokit, params, retries + 1)
27 | }
28 | throw error
29 | }
30 | }
31 |
32 | export async function fetchCodebaseForBranch(data: {
33 | githubRepoFullName: string
34 | path: string
35 | branch: string
36 | installationId: number | null
37 | }) {
38 | try {
39 | const contents = await fetchDirectoryContent(data)
40 |
41 | const files: GitHubFile[] = []
42 |
43 | const [owner, repo] = data.githubRepoFullName.split("/")
44 |
45 | for (const item of contents) {
46 | if (item.type === "file") {
47 | files.push({
48 | ...item,
49 | owner,
50 | repo,
51 | ref: data.branch
52 | })
53 | } else if (item.type === "dir" && !item.name.startsWith(".")) {
54 | try {
55 | const nestedFiles = await fetchCodebaseForBranch({
56 | ...data,
57 | path: item.path
58 | })
59 | files.push(...nestedFiles)
60 | } catch (error) {
61 | console.error(`Error fetching nested files for ${item.path}:`, error)
62 | throw error
63 | }
64 | }
65 | }
66 |
67 | return files
68 | } catch (error) {
69 | console.error(`Error fetching codebase for path ${data.path}:`, error)
70 | throw error
71 | }
72 | }
73 |
74 | async function fetchDirectoryContent(data: {
75 | githubRepoFullName: string
76 | path: string
77 | branch: string
78 | installationId: number | null
79 | }) {
80 | const [organization, repo] = data.githubRepoFullName.split("/")
81 |
82 | try {
83 | const octokit = await getAuthenticatedOctokit(data.installationId)
84 | const { data: content } = await fetchWithRetry(octokit, {
85 | owner: organization,
86 | repo,
87 | path: data.path,
88 | ref: data.branch
89 | })
90 |
91 | return Array.isArray(content) ? content : [content]
92 | } catch (error) {
93 | console.error("Error fetching directory content:", error)
94 | throw error
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ChevronLeft, ChevronRight } from "lucide-react"
4 | import * as React from "react"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { buttonVariants } from "@/components/ui/button"
8 | import { cn } from "@/lib/utils"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
58 | IconRight: () =>
59 | }}
60 | {...props}
61 | />
62 | )
63 | }
64 | Calendar.displayName = "Calendar"
65 |
66 | export { Calendar }
67 |
--------------------------------------------------------------------------------
/components/dashboard/reusable/crud-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { Card, CardContent } from "@/components/ui/card"
5 | import { Input } from "@/components/ui/input"
6 | import { useRouter } from "next/navigation"
7 | import { FC, useState } from "react"
8 | import ReactTextareaAutosize from "react-textarea-autosize"
9 |
10 | interface CRUDFormProps {
11 | itemName: string
12 | buttonText: string
13 | onSubmit: (formData: FormData) => Promise
14 | data?: {
15 | name: string
16 | content: string
17 | }
18 | }
19 |
20 | export const CRUDForm: FC = ({
21 | itemName,
22 | buttonText,
23 | onSubmit,
24 | data
25 | }) => {
26 | const router = useRouter()
27 | const [isSubmitting, setIsSubmitting] = useState(false)
28 | const [name, setName] = useState(data?.name || "")
29 | const [content, setContent] = useState(data?.content || "")
30 |
31 | const isFormValid = name.trim() !== "" && content.trim() !== ""
32 |
33 | const handleSubmit = async (event: React.MouseEvent) => {
34 | event.preventDefault()
35 | if (!isFormValid) return
36 |
37 | setIsSubmitting(true)
38 | const form = event.currentTarget.closest("form")
39 | if (form) {
40 | const formData = new FormData(form)
41 | await onSubmit(formData)
42 | }
43 | setIsSubmitting(false)
44 | }
45 |
46 | return (
47 |
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/lib/ai/build-plan-prompt.ts:
--------------------------------------------------------------------------------
1 | import endent from "endent"
2 | import { limitTokens } from "./limit-tokens"
3 |
4 | export const buildCodePlanPrompt = async ({
5 | issue,
6 | codebaseFiles,
7 | instructionsContext
8 | }: {
9 | issue: {
10 | name: string
11 | description: string
12 | }
13 | codebaseFiles: {
14 | path: string
15 | content: string
16 | }[]
17 | instructionsContext: string
18 | }): Promise => {
19 | const basePrompt = endent`
20 | # AI Task Planning Assistant
21 |
22 | You are an AI specialized in creating detailed implementation plans for coding tasks.
23 |
24 | You will be given a task, codebase, and instructions.
25 |
26 | Your goal is to break down the given issue into clear, actionable steps that another developer can follow to complete the task.
27 |
28 | Create a detailed implementation plan for the given issue. Your plan should:
29 |
30 | - Stick to the task at hand.
31 | - Break down the task into clear, logical steps.
32 | - Ensure the plan is detailed enough to allow another developer to implement the task.
33 | - Be 100% correct and complete.
34 |
35 | Note: Focus solely on the technical implementation. Ignore any mentions of human tasks or non-technical aspects.
36 |
37 | Encoded in XML tags, here is what you will be given:
38 |
39 | TASK: Information about the task.
40 | CODEBASE: Files from the codebase.
41 | INSTRUCTIONS: Instructions and guidelines on how to complete the task.
42 | FORMAT: Instructions on how to format your response.
43 |
44 | ---
45 |
46 | # Task
47 |
48 |
49 |
50 | # Title
51 | ${issue.name ?? "No title."}
52 |
53 | ## Description
54 | ${issue.description ?? "No description."}
55 |
56 |
57 |
58 | ---
59 |
60 | # Codebase
61 |
62 |
63 | `
64 |
65 | const formatInstructions = endent`
66 |
67 |
68 | ---
69 |
70 | # Instructions
71 |
72 |
73 |
74 | Follow these instructions:
75 |
76 | ${instructionsContext}
77 |
78 |
79 |
80 | ---
81 |
82 | # Format
83 |
84 |
85 |
86 | Format your response as follows:
87 |
88 | 1. Present your plan as a numbered list of steps.
89 | 2. Use markdown formatting.
90 |
91 |
92 | `
93 |
94 | const { prompt, tokensUsed } = limitTokens(basePrompt, codebaseFiles)
95 | const finalPrompt = `${prompt}\n${formatInstructions}`
96 | console.warn(`Code Plan Prompt: Tokens used: ${tokensUsed}`)
97 |
98 | return finalPrompt
99 | }
100 |
--------------------------------------------------------------------------------
/db/queries/templates-queries.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { getUserId } from "@/actions/auth/auth"
4 | import { eq } from "drizzle-orm"
5 | import { revalidatePath } from "next/cache"
6 | import { db } from "../db"
7 | import {
8 | InsertTemplate,
9 | SelectTemplate,
10 | templatesTable
11 | } from "../schema/templates-schema"
12 |
13 | export async function createTemplateRecords(
14 | data: Omit[]
15 | ): Promise {
16 | const userId = await getUserId()
17 |
18 | try {
19 | const result = await db
20 | .insert(templatesTable)
21 | .values(data.map(template => ({ ...template, userId })))
22 | .returning()
23 | revalidatePath("/")
24 | return result
25 | } catch (error) {
26 | console.error("Error creating template records:", error)
27 | throw error
28 | }
29 | }
30 |
31 | export async function getTemplateById(
32 | id: string
33 | ): Promise {
34 | try {
35 | return db.query.templates.findFirst({
36 | where: eq(templatesTable.id, id)
37 | })
38 | } catch (error) {
39 | console.error(`Error getting template by id ${id}:`, error)
40 | throw error
41 | }
42 | }
43 |
44 | export async function getTemplatesWithInstructionsByProjectId(
45 | projectId: string
46 | ) {
47 | try {
48 | const results = await db.query.templates.findMany({
49 | with: {
50 | templatesToInstructions: {
51 | with: {
52 | instruction: true
53 | }
54 | }
55 | },
56 | where: (templates, { eq }) => eq(templates.projectId, projectId),
57 | orderBy: (templates, { desc }) => desc(templates.updatedAt)
58 | })
59 | return results
60 | } catch (error) {
61 | console.error("Error getting templates for user and project:", error)
62 | throw error
63 | }
64 | }
65 |
66 | export async function getTemplateWithInstructionById(id: string) {
67 | try {
68 | return db.query.templates.findFirst({
69 | with: {
70 | templatesToInstructions: {
71 | with: {
72 | instruction: true
73 | }
74 | }
75 | },
76 | where: eq(templatesTable.id, id)
77 | })
78 | } catch (error) {
79 | console.error(`Error getting template by id ${id}:`, error)
80 | throw error
81 | }
82 | }
83 |
84 | export async function updateTemplate(
85 | id: string,
86 | data: Partial>,
87 | projectId: string
88 | ): Promise {
89 | try {
90 | await db
91 | .update(templatesTable)
92 | .set({ ...data, projectId })
93 | .where(eq(templatesTable.id, id))
94 | revalidatePath("/")
95 | } catch (error) {
96 | console.error(`Error updating template ${id}:`, error)
97 | throw error
98 | }
99 | }
100 |
101 | export async function deleteTemplate(id: string): Promise {
102 | try {
103 | await db.delete(templatesTable).where(eq(templatesTable.id, id))
104 | revalidatePath("/")
105 | } catch (error) {
106 | console.error(`Error deleting template ${id}:`, error)
107 | throw error
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/components/ui/multi-select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import {
5 | Command,
6 | CommandEmpty,
7 | CommandGroup,
8 | CommandInput,
9 | CommandItem,
10 | CommandList
11 | } from "@/components/ui/command"
12 | import {
13 | Popover,
14 | PopoverContent,
15 | PopoverTrigger
16 | } from "@/components/ui/popover"
17 | import { cn } from "@/lib/utils"
18 | import { Check, ChevronsUpDown } from "lucide-react"
19 | import { useState } from "react"
20 |
21 | interface MultiSelectProps {
22 | label: string
23 | data: {
24 | id: string
25 | name: string
26 | }[]
27 | selectedIds: string[]
28 | onToggleSelect: (selectedIds: string[]) => void
29 | }
30 |
31 | export function MultiSelect({
32 | label,
33 | data,
34 | selectedIds,
35 | onToggleSelect
36 | }: MultiSelectProps) {
37 | const [open, setOpen] = useState(false)
38 |
39 | const handleToggleSelect = async (id: string) => {
40 | onToggleSelect(
41 | selectedIds.includes(id)
42 | ? selectedIds.filter(selectedId => selectedId !== id)
43 | : [...selectedIds, id]
44 | )
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 |
65 |
66 |
67 |
68 |
69 |
70 | No {label} found.
71 |
72 | {data.length > 0 ? (
73 | data.map(item => {
74 | return (
75 | handleToggleSelect(item.id)}
78 | >
79 |
87 | {item.name}
88 |
89 | )
90 | })
91 | ) : (
92 | No {label}s available
93 | )}
94 |
95 |
96 |
97 |
98 |
99 |
100 | )
101 | }
102 |
--------------------------------------------------------------------------------