├── .actrc ├── .changeset ├── README.md ├── bright-ducks-help.md ├── clean-drinks-laugh.md ├── config.json ├── dull-bugs-cry.md ├── dull-buttons-draw.md ├── dull-yaks-worry.md ├── fast-coins-unite.md ├── hot-kangaroos-dream.md ├── odd-phones-shave.md ├── popular-rules-beam.md ├── popular-yaks-sing.md ├── pre.json ├── quick-rice-care.md ├── silver-pumas-dance.md ├── slimy-bikes-divide.md ├── strange-books-battle.md ├── strange-pandas-applaud.md ├── stupid-bags-eat.md └── tiny-kings-sparkle.md ├── .env.example ├── .floe ├── config.json └── rules │ ├── docs-style.md │ └── spelling-and-grammar.md ├── .github └── workflows │ └── review.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── act ├── .env.act.example └── event.pull_request.json ├── actions ├── README.md └── review-action │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── action.yml │ ├── dist │ ├── actions │ │ └── review-action │ │ │ └── src │ │ │ ├── index.d.ts │ │ │ ├── index.d.ts.map │ │ │ ├── types.d.ts │ │ │ └── types.d.ts.map │ ├── index.js │ ├── index.js.map │ ├── licenses.txt │ ├── packages │ │ ├── lib │ │ │ ├── diff-parser.d.ts │ │ │ ├── diff-parser.d.ts.map │ │ │ ├── get-floe-config.d.ts │ │ │ ├── get-floe-config.d.ts.map │ │ │ ├── not-empty.d.ts │ │ │ ├── not-empty.d.ts.map │ │ │ ├── pluralize.d.ts │ │ │ ├── pluralize.d.ts.map │ │ │ ├── rules.d.ts │ │ │ └── rules.d.ts.map │ │ └── requests │ │ │ ├── api.d.ts │ │ │ ├── api.d.ts.map │ │ │ ├── git │ │ │ ├── issue-comments │ │ │ │ ├── _get.d.ts │ │ │ │ ├── _get.d.ts.map │ │ │ │ ├── _post.d.ts │ │ │ │ └── _post.d.ts.map │ │ │ └── review-comments │ │ │ │ ├── _get.d.ts │ │ │ │ ├── _get.d.ts.map │ │ │ │ ├── _post.d.ts │ │ │ │ └── _post.d.ts.map │ │ │ └── review │ │ │ ├── _post.d.ts │ │ │ └── _post.d.ts.map │ └── sourcemap-register.js │ ├── package.json │ ├── src │ ├── index.ts │ └── types.ts │ └── tsconfig.json ├── apps ├── api │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── sentry.client.config.ts │ ├── sentry.edge.config.ts │ ├── sentry.server.config.ts │ ├── src │ │ ├── constants │ │ │ ├── ignore-list.ts │ │ │ └── supported-files.ts │ │ ├── lib │ │ │ ├── ai │ │ │ │ └── index.ts │ │ │ ├── github │ │ │ │ ├── compare.ts │ │ │ │ ├── contents.ts │ │ │ │ └── octokit.ts │ │ │ ├── gitlab │ │ │ │ └── compare.ts │ │ │ ├── middleware │ │ │ │ ├── ai-rate-limiter.ts │ │ │ │ ├── api-id.ts │ │ │ │ ├── authenticate.ts │ │ │ │ ├── capture-errors.ts │ │ │ │ ├── default-handler.ts │ │ │ │ ├── default-responder.ts │ │ │ │ ├── ip-rate-limiter.ts │ │ │ │ ├── qs.ts │ │ │ │ └── with-middlware.ts │ │ │ └── normalizedGitProviders │ │ │ │ ├── compare.ts │ │ │ │ ├── content.ts │ │ │ │ └── strings.ts │ │ ├── pages │ │ │ └── api │ │ │ │ ├── ai-create-diff │ │ │ │ ├── _get.ts │ │ │ │ └── index.ts │ │ │ │ ├── git │ │ │ │ ├── issue-comments │ │ │ │ │ ├── [comment_id] │ │ │ │ │ │ ├── _patch.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── _get.ts │ │ │ │ │ ├── _post.ts │ │ │ │ │ └── index.ts │ │ │ │ └── review-comments │ │ │ │ │ ├── README.md │ │ │ │ │ ├── _get.ts │ │ │ │ │ ├── _post.ts │ │ │ │ │ └── index.ts │ │ │ │ └── review │ │ │ │ ├── _post.ts │ │ │ │ ├── example.ts │ │ │ │ ├── index.ts │ │ │ │ └── prompts.ts │ │ ├── types │ │ │ ├── compare.ts │ │ │ └── middleware.ts │ │ └── utils │ │ │ ├── checksum.ts │ │ │ ├── get-cache-key.ts │ │ │ ├── handlebars.ts │ │ │ ├── string-to-lines.ts │ │ │ └── z-parse.ts │ ├── tsconfig.json │ └── vercel.json ├── app │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── github.png │ │ └── logo.png │ ├── src │ │ ├── app │ │ │ ├── (authenticated) │ │ │ │ ├── [workspace] │ │ │ │ │ ├── billing │ │ │ │ │ │ ├── actions.ts │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── developers │ │ │ │ │ │ ├── keys │ │ │ │ │ │ │ ├── actions.ts │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ ├── key-modal.tsx │ │ │ │ │ │ │ └── table.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── integrations │ │ │ │ │ │ ├── actions.ts │ │ │ │ │ │ ├── github-button.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── nav.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── usage.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── new │ │ │ │ │ ├── actions.ts │ │ │ │ │ ├── context.tsx │ │ │ │ │ ├── nav.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ ├── step-1.tsx │ │ │ │ │ └── step-2.tsx │ │ │ │ └── page.tsx │ │ │ ├── _components │ │ │ │ ├── header.tsx │ │ │ │ └── magic-link-email.tsx │ │ │ ├── api │ │ │ │ ├── auth │ │ │ │ │ └── [...nextauth] │ │ │ │ │ │ └── route.ts │ │ │ │ ├── installation-callback │ │ │ │ │ ├── get-octokit.ts │ │ │ │ │ ├── handle-setup-install-with-state.ts │ │ │ │ │ ├── handle-setup-install-without-state.ts │ │ │ │ │ ├── handle-setup-request-with-state.ts │ │ │ │ │ ├── handle-setup-request-without-state.ts │ │ │ │ │ ├── route.ts │ │ │ │ │ └── schema.ts │ │ │ │ └── webhooks │ │ │ │ │ └── stripe │ │ │ │ │ └── route.ts │ │ │ ├── installation │ │ │ │ ├── confirmed │ │ │ │ │ └── page.tsx │ │ │ │ └── requested │ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── signin │ │ │ │ ├── form.tsx │ │ │ │ └── page.tsx │ │ │ └── verify-request │ │ │ │ └── page.tsx │ │ ├── env.mjs │ │ ├── lib │ │ │ ├── features │ │ │ │ ├── github-installation.ts │ │ │ │ └── workspace.ts │ │ │ └── stripe │ │ │ │ └── index.ts │ │ ├── server │ │ │ └── auth.ts │ │ ├── styles │ │ │ └── globals.css │ │ └── utils │ │ │ └── url.ts │ ├── tailwind.config.js │ └── tsconfig.json └── web │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── client.webpack.lock │ ├── components │ ├── authors.tsx │ ├── blog │ │ ├── list.tsx │ │ └── post.tsx │ ├── footer.tsx │ ├── home │ │ ├── blob.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── feature-card.tsx │ │ ├── index.tsx │ │ ├── nav.tsx │ │ └── title.tsx │ └── pricing │ │ └── index.tsx │ ├── globals.css │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _meta.json │ ├── blog.mdx │ ├── blog │ │ ├── _meta.json │ │ ├── first-post.mdx │ │ └── second-post.mdx │ ├── docs │ │ ├── _meta.json │ │ ├── ci.mdx │ │ ├── cli.mdx │ │ ├── configuration.mdx │ │ ├── index.mdx │ │ ├── installation.mdx │ │ ├── prerequisites.mdx │ │ ├── quick-start.mdx │ │ └── usage.mdx │ ├── index.mdx │ └── pricing.mdx │ ├── postcss.config.js │ ├── public │ ├── avatar-nic.jpeg │ ├── ci-example.png │ ├── favicon.ico │ ├── itc-garamond-std.woff2 │ ├── logo-title.svg │ ├── logo.svg │ ├── noise.svg │ └── pencil-art.png │ ├── server.webpack.lock │ ├── tailwind.config.js │ ├── theme.config.jsx │ ├── tsconfig.json │ ├── types │ └── frontmatter.ts │ ├── video.d.ts │ └── videos │ ├── custom-rules.mp4.json │ └── review-fix.mp4.json ├── package.json ├── packages ├── cli │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── build.js │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── ai-create │ │ │ │ ├── diff.ts │ │ │ │ └── index.ts │ │ │ ├── init.ts │ │ │ └── review │ │ │ │ ├── diff.ts │ │ │ │ ├── files.ts │ │ │ │ ├── index.ts │ │ │ │ └── lib.ts │ │ ├── default-files │ │ │ ├── rules │ │ │ │ └── spelling-and-grammar.md │ │ │ └── templates │ │ │ │ └── release-note.md │ │ ├── index.ts │ │ └── utils │ │ │ ├── git.ts │ │ │ ├── lines-update.ts │ │ │ ├── logging.ts │ │ │ └── truncate.ts │ └── tsconfig.json ├── config │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── generate.ts │ ├── index.ts │ ├── package.json │ ├── schema.json │ ├── schema.module.ts │ ├── tsconfig.json │ ├── types.ts │ └── validate.ts ├── db │ ├── .env.example │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── index.ts │ ├── models │ │ ├── index.ts │ │ ├── price │ │ │ └── index.ts │ │ ├── product │ │ │ └── index.ts │ │ ├── subscription │ │ │ ├── constants.ts │ │ │ └── index.ts │ │ └── token-usage │ │ │ ├── get-month-year.ts │ │ │ └── index.ts │ ├── package.json │ ├── prisma │ │ ├── schema.prisma │ │ └── seed.ts │ └── tsconfig.json ├── eslint-config-custom │ ├── CHANGELOG.md │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── features │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── package.json │ ├── reviews │ │ └── index.ts │ └── tsconfig.json ├── lib │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── capitalize.ts │ ├── check-if-valid-root.ts │ ├── class-names.ts │ ├── diff-parser.ts │ ├── encryption.ts │ ├── get-floe-config.ts │ ├── get-month-year.ts │ ├── http-error.ts │ ├── not-empty.ts │ ├── package.json │ ├── pluralize.ts │ ├── rules.ts │ ├── slugify.ts │ └── tsconfig.json ├── requests │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── ai-create-diff │ │ └── _get.ts │ ├── api.ts │ ├── git │ │ ├── issue-comments │ │ │ ├── [comment_id] │ │ │ │ └── _patch.ts │ │ │ ├── _get.ts │ │ │ └── _post.ts │ │ └── review-comments │ │ │ ├── _get.ts │ │ │ └── _post.ts │ ├── package.json │ ├── review │ │ └── _post.ts │ └── tsconfig.json ├── tailwind │ ├── CHANGELOG.md │ ├── package.json │ ├── postcss.config.js │ └── tailwind.config.js ├── tsconfig │ ├── CHANGELOG.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.js │ ├── CHANGELOG.md │ ├── accordion.tsx │ ├── action-card.tsx │ ├── button.tsx │ ├── card.tsx │ ├── clipboard.tsx │ ├── index.ts │ ├── input.tsx │ ├── modal.tsx │ ├── package.json │ ├── pill.tsx │ ├── ping.tsx │ ├── postcss.config.js │ ├── spinner.tsx │ ├── tailwind.config.js │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.actrc: -------------------------------------------------------------------------------- 1 | -P ubuntu-latest=catthehacker/ubuntu:act-latest 2 | -e act/event.pull_request.json 3 | --env-file act/.env.act 4 | --container-architecture linux/amd64 5 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/bright-ducks-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/api": minor 3 | "@floe/app": minor 4 | "@floe/docs": minor 5 | "@floe/web": minor 6 | "@floe/action": minor 7 | "@floe/cli": minor 8 | "@floe/config": minor 9 | "@floe/db": minor 10 | "eslint-config-custom": minor 11 | "@floe/lib": minor 12 | "@floe/requests": minor 13 | "@floe/tailwind": minor 14 | "tsconfig": minor 15 | "@floe/ui": minor 16 | --- 17 | 18 | Bump to beta version. 19 | -------------------------------------------------------------------------------- /.changeset/clean-drinks-laugh.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/requests": minor 3 | --- 4 | 5 | Make @floe/requests public. 6 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [["@floe/cli", "@floe/config"]], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/dull-bugs-cry.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/review-action": minor 3 | --- 4 | 5 | Modify diff lookup strategy to support forked braches in PRs. 6 | -------------------------------------------------------------------------------- /.changeset/dull-buttons-draw.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/cli": patch 3 | --- 4 | 5 | Fix issue where Floe CLI would not exit after encountering too many files in CLI. 6 | -------------------------------------------------------------------------------- /.changeset/dull-yaks-worry.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/cli": minor 3 | --- 4 | 5 | Default config should check for all .md and .mdx files. 6 | -------------------------------------------------------------------------------- /.changeset/fast-coins-unite.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/cli": minor 3 | "@floe/config": minor 4 | --- 5 | 6 | Cut a new release for cli and config 7 | -------------------------------------------------------------------------------- /.changeset/hot-kangaroos-dream.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/api": minor 3 | "@floe/cli": minor 4 | "@floe/features": minor 5 | "@floe/requests": minor 6 | --- 7 | 8 | Stability fixes to reviews. 9 | -------------------------------------------------------------------------------- /.changeset/odd-phones-shave.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/review-action": minor 3 | "@floe/cli": minor 4 | "@floe/requests": minor 5 | --- 6 | 7 | Update some API paths. This introduces a breaking change to the CLI ‼️ 8 | -------------------------------------------------------------------------------- /.changeset/popular-rules-beam.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/api": minor 3 | "@floe/app": minor 4 | "@floe/docs": minor 5 | "@floe/web": minor 6 | "@floe/cli": minor 7 | "@floe/config": minor 8 | "@floe/db": minor 9 | "@floe/embeddings-action": minor 10 | "eslint-config-custom": minor 11 | "@floe/tailwind": minor 12 | "tsconfig": minor 13 | "@floe/requests": minor 14 | "@floe/ui": minor 15 | --- 16 | 17 | Create initial alpha version. 18 | -------------------------------------------------------------------------------- /.changeset/popular-yaks-sing.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/review-action": minor 3 | "@floe/features": minor 4 | "@floe/requests": minor 5 | "@floe/cli": minor 6 | "@floe/lib": minor 7 | "@floe/db": minor 8 | "@floe/ui": minor 9 | "@floe/api": minor 10 | "@floe/app": minor 11 | --- 12 | 13 | Add support for token usage and pro / basic models. 14 | -------------------------------------------------------------------------------- /.changeset/pre.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "pre", 3 | "tag": "beta", 4 | "initialVersions": { 5 | "@floe/api": "0.1.0-alpha.2", 6 | "@floe/app": "0.1.0-alpha.2", 7 | "@floe/docs": "0.1.0-alpha.0", 8 | "@floe/web": "0.1.0-alpha.1", 9 | "@floe/action": "0.1.0-alpha.2", 10 | "@floe/cli": "0.1.0-alpha.6", 11 | "@floe/config": "0.1.0-alpha.5", 12 | "@floe/db": "0.1.0-alpha.0", 13 | "eslint-config-custom": "0.1.0-alpha.0", 14 | "@floe/lib": "0.1.0-alpha.1", 15 | "@floe/requests": "0.1.0-alpha.1", 16 | "@floe/tailwind": "0.1.0-alpha.0", 17 | "tsconfig": "0.1.0-alpha.0", 18 | "@floe/ui": "0.1.0-alpha.1", 19 | "@floe/review-action": "0.1.0-beta.1", 20 | "@floe/features": "0.1.0-beta.1" 21 | }, 22 | "changesets": [ 23 | "bright-ducks-help", 24 | "clean-drinks-laugh", 25 | "dull-bugs-cry", 26 | "dull-buttons-draw", 27 | "dull-yaks-worry", 28 | "fast-coins-unite", 29 | "hot-kangaroos-dream", 30 | "odd-phones-shave", 31 | "popular-rules-beam", 32 | "popular-yaks-sing", 33 | "quick-rice-care", 34 | "silver-pumas-dance", 35 | "slimy-bikes-divide", 36 | "strange-books-battle", 37 | "strange-pandas-applaud", 38 | "stupid-bags-eat", 39 | "tiny-kings-sparkle" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.changeset/quick-rice-care.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/review-action": minor 3 | --- 4 | 5 | Cut a new release. 6 | -------------------------------------------------------------------------------- /.changeset/silver-pumas-dance.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/config": minor 3 | --- 4 | 5 | Change config schema. 6 | -------------------------------------------------------------------------------- /.changeset/slimy-bikes-divide.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/cli": minor 3 | "@floe/lib": minor 4 | "@floe/api": minor 5 | "@floe/app": minor 6 | --- 7 | 8 | Improve error handling. 9 | -------------------------------------------------------------------------------- /.changeset/strange-books-battle.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/cli": minor 3 | "@floe/config": minor 4 | "@floe/lib": minor 5 | --- 6 | 7 | Adds CLI 'floe review files' command. 8 | -------------------------------------------------------------------------------- /.changeset/strange-pandas-applaud.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/cli": minor 3 | --- 4 | 5 | Give Floe init a success status. 6 | -------------------------------------------------------------------------------- /.changeset/stupid-bags-eat.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/review-action": minor 3 | "@floe/api": minor 4 | "@floe/app": minor 5 | "@floe/cli": minor 6 | "@floe/db": minor 7 | "@floe/features": minor 8 | "@floe/lib": minor 9 | "@floe/requests": minor 10 | "@floe/ui": minor 11 | --- 12 | 13 | Add support for token usage and pro / basic models. 14 | -------------------------------------------------------------------------------- /.changeset/tiny-kings-sparkle.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@floe/cli": minor 3 | --- 4 | 5 | Implement new review endpoint in CLI 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLOE_API_WORKSPACE= 2 | FLOE_API_SECRET= 3 | FLOE_API_ENDPOINT=http://localhost:4000 4 | -------------------------------------------------------------------------------- /.floe/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@floe/config@0.1.0-beta.8/schema.json", 3 | "reviews": { 4 | "maxFileEvaluations": 5, 5 | "maxDiffEvaluations": 20 6 | }, 7 | "rulesets": { 8 | "docs": { 9 | "include": ["apps/web/**/*.{md,mdx}", "./README.md"], 10 | "rules": { 11 | "spelling-and-grammar": "warn", 12 | "docs-style": "warn" 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.floe/rules/docs-style.md: -------------------------------------------------------------------------------- 1 | - Don't use "we" or "I" 2 | -------------------------------------------------------------------------------- /.floe/rules/spelling-and-grammar.md: -------------------------------------------------------------------------------- 1 | Make sure to use proper spelling and grammar. 2 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: "Floe Review" 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | Review: 7 | env: 8 | FLOE_API_WORKSPACE: ${{ secrets.FLOE_API_WORKSPACE }} 9 | FLOE_API_SECRET: ${{ secrets.FLOE_API_SECRET }} 10 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Review 17 | uses: ./actions/review-action 18 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env 27 | .env.act 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo 35 | 36 | # vercel 37 | .vercel 38 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 | 6 |

Floe

7 | 8 |

9 | AI writing assistants for docs and changelogs. 10 |
11 | Learn more » 12 |
13 |
14 | Slack 15 | · 16 | Website 17 | · 18 | Issues 19 |

20 |

21 | 22 | ## About Floe 23 | Floe is an AI writing assistant for your CLI / CI. Floe can help you to ship technical content, like docs and changelogs, with higher quality and less effort. 24 | 25 | Floe ships with two core features: 26 | 27 | - **Reviews**: Write your own rules, in plain English, to check your content for a variety of issues. Floe will automatically review your content and provide feedback. 28 | 29 | - **First Draft (Coming soon)**: Floe can generate a first draft of your content, based on PR context and templates you control. 30 | 31 |
32 | 33 | https://github.com/Floe-dev/floe/assets/9045634/17d0691a-52d9-4bc3-9b2a-83756222eba8 34 | 35 |
36 | 37 | https://github.com/Floe-dev/floe/assets/9045634/7244688a-ea51-4cc6-880a-2075bd4845b0 38 | 39 | 40 | ## Built-with 41 | 42 | - [Next.js](https://nextjs.org/) 43 | - [React.js](https://reactjs.org/) 44 | - [Tailwind CSS](https://tailwindcss.com/) 45 | - [Prisma.io](https://prisma.io/) 46 | - [Turborepo](https://turbo.build/repo/) 47 | - [NextAuth](https://next-auth.js.org/) 48 | 49 | ## Documentation 50 | Checkout the [Floe docs](https://floe.dev/docs) to get started. 51 | 52 | ## Contributing 53 | Floe is open to contributions! Guidelines in progress. 54 | 55 | ## Contact 56 | You can get in touch with me by email, or feel free to book a call. 57 | 58 | [📅 Book a demo](https://cal.com/nic-haley/book-a-demo) 59 | 60 | [📨 Contact](mailto:nic@floe.dev) 61 | -------------------------------------------------------------------------------- /act/.env.act.example: -------------------------------------------------------------------------------- 1 | # This is meant to simulate Secrets for GitHub Actions when using Act 2 | # More info: https://github.com/nektos/act#secrets 3 | FLOE_API_WORKSPACE= 4 | FLOE_API_SECRET= 5 | GITHUB_HEAD_REF= 6 | GITHUB_BASE_REF= 7 | FLOE_API_ENDPOINT= 8 | FLOE_TEST_MODE=1 -------------------------------------------------------------------------------- /act/event.pull_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": { 3 | "owner": { 4 | "login": "Floe-dev" 5 | }, 6 | "name": "floe" 7 | }, 8 | "pull_request": { 9 | "number": 69, 10 | "head": { 11 | "sha": "0532e0ad946a3e1db4ae5f2c94d4c00afff01870" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /actions/README.md: -------------------------------------------------------------------------------- 1 | # Embeddings Action ⚠️ EXPERIMENTAL ⚠️ 2 | 3 | This project is purely experimental. It is a GitHub Action that generates embeddings and stores them in Pinccone for documentation. This can later be used to semantically search the docs to see if content should be updated for a new set of changes. 4 | 5 | This project needs a lot more work. Next steps would be: 6 | 7 | - [ ] Chunk documents into smaller pieces 8 | - [ ] Store chunks with line numbers + filenames 9 | - [ ] Move Pinecone inserts and similarity seach to API 10 | 11 | ## Usage 12 | 13 | 1. Run Docker 14 | 2. Run `ngrok http 4000` and add URL to act/env.act 15 | 3. Run from root using Act: 16 | 17 | ```bash 18 | act 19 | ``` 20 | 21 | Note: It may take a while to start 22 | -------------------------------------------------------------------------------- /actions/review-action/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/library"], 3 | }; 4 | -------------------------------------------------------------------------------- /actions/review-action/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/actions/review-action/.gitignore -------------------------------------------------------------------------------- /actions/review-action/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @floe/review-action 2 | 3 | ## 0.1.0-beta.8 4 | 5 | ### Minor Changes 6 | 7 | - Add support for token usage and pro / basic models. 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @floe/features@0.1.0-beta.6 13 | - @floe/lib@0.1.0-beta.5 14 | - @floe/requests@0.1.0-beta.7 15 | 16 | ## 0.1.0-beta.7 17 | 18 | ### Minor Changes 19 | 20 | - 5f84851: Add support for token usage and pro / basic models. 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [5f84851] 25 | - @floe/features@0.1.0-beta.5 26 | - @floe/requests@0.1.0-beta.6 27 | - @floe/lib@0.1.0-beta.4 28 | 29 | ## 0.1.0-beta.6 30 | 31 | ### Minor Changes 32 | 33 | - b7e69cc: Modify diff lookup strategy to support forked braches in PRs. 34 | 35 | ## 0.1.0-beta.5 36 | 37 | ### Minor Changes 38 | 39 | - Cut a new release. 40 | 41 | ## 0.1.0-beta.4 42 | 43 | ### Minor Changes 44 | 45 | - Update some API paths. This introduces a breaking change to the CLI ‼️ 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies 50 | - @floe/requests@0.1.0-beta.5 51 | - @floe/features@0.1.0-beta.4 52 | 53 | ## 0.1.0-beta.3 54 | 55 | ### Patch Changes 56 | 57 | - Updated dependencies [2cae03a] 58 | - @floe/requests@0.1.0-beta.4 59 | - @floe/features@0.1.0-beta.3 60 | 61 | ## 0.1.0-beta.2 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies 66 | - @floe/features@0.1.0-beta.2 67 | - @floe/requests@0.1.0-beta.3 68 | -------------------------------------------------------------------------------- /actions/review-action/action.yml: -------------------------------------------------------------------------------- 1 | name: "Floe Validator" 2 | description: "Floe Validator" 3 | 4 | runs: 5 | using: "node20" 6 | main: "./dist/index.js" 7 | -------------------------------------------------------------------------------- /actions/review-action/dist/actions/review-action/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=index.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/actions/review-action/src/index.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/actions/review-action/src/index.ts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /actions/review-action/dist/actions/review-action/src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Inputs { 2 | token: string; 3 | } 4 | //# sourceMappingURL=types.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/actions/review-action/src/types.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/actions/review-action/src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;CACf"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/diff-parser.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a list of Files containing Hunks 3 | */ 4 | export declare function parseDiffToFileHunks(diffText: string): { 5 | path: string; 6 | hunks: { 7 | startLine: number; 8 | content: string; 9 | }[]; 10 | }[]; 11 | export type File = ReturnType[number]; 12 | //# sourceMappingURL=diff-parser.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/diff-parser.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"diff-parser.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/lib/diff-parser.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM;;;;;;IA4BpD;AAED,MAAM,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/get-floe-config.d.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@floe/config"; 2 | export declare function getFloeConfig(): Config; 3 | //# sourceMappingURL=get-floe-config.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/get-floe-config.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"get-floe-config.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/lib/get-floe-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,wBAAgB,aAAa,WAa5B"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/not-empty.d.ts: -------------------------------------------------------------------------------- 1 | export declare function notEmpty(value: TValue | null | undefined): value is TValue; 2 | //# sourceMappingURL=not-empty.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/not-empty.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"not-empty.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/lib/not-empty.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,MAAM,EAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,KAAK,IAAI,MAAM,CAEjB"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/pluralize.d.ts: -------------------------------------------------------------------------------- 1 | export declare function pluralize(count: number, singular: string, plural: string): string; 2 | //# sourceMappingURL=pluralize.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/pluralize.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"pluralize.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/lib/pluralize.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAExE"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/rules.d.ts: -------------------------------------------------------------------------------- 1 | export declare const getRulesets: (ruleset?: string) => { 2 | rules: { 3 | code: string; 4 | level: "error" | "warn"; 5 | description: string; 6 | }[]; 7 | include: readonly string[]; 8 | name: string; 9 | }[]; 10 | export type Ruleset = ReturnType[number]; 11 | //# sourceMappingURL=rules.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/packages/lib/rules.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/lib/rules.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,WAAW,aAAc,MAAM;;;;;;;;GAyC3C,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/api.d.ts: -------------------------------------------------------------------------------- 1 | export declare function getBaseUrl(): string; 2 | export declare const api: import("axios").AxiosInstance; 3 | //# sourceMappingURL=api.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/api.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/requests/api.ts"],"names":[],"mappings":"AAEA,wBAAgB,UAAU,WAOzB;AAKD,eAAO,MAAM,GAAG,+BAMd,CAAC"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/git/issue-comments/_get.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"_get.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/requests/git/issue-comments/_get.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,WAAW;;;;;;;;;;;;EAItB,CAAC;AAEH,MAAM,MAAM,2BAA2B,GACrC,SAAS,CAAC,2CAA2C,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7E,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhE,wBAAsB,qBAAqB,CAAC,EAC1C,KAAK,EACL,IAAI,EACJ,WAAW,GACZ,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAQvB"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/git/issue-comments/_post.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"_post.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/requests/git/issue-comments/_post.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;EAKtB,CAAC;AAEH,MAAM,MAAM,4BAA4B,GACtC,SAAS,CAAC,2DAA2D,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7F,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEjE,wBAAsB,qBAAqB,CAAC,EAC1C,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,WAAW,GACZ,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SASxB"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/git/review-comments/_get.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"_get.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/requests/git/review-comments/_get.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,WAAW;;;;;;;;;;;;EAItB,CAAC;AAEH,MAAM,MAAM,4BAA4B,GACtC,SAAS,CAAC,wDAAwD,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1F,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEjE,wBAAsB,sBAAsB,CAAC,EAC3C,KAAK,EACL,IAAI,EACJ,UAAU,GACX,EAAE,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAQxB"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/git/review-comments/_post.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"_post.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/requests/git/review-comments/_post.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWtB,CAAC;AAEH,MAAM,MAAM,6BAA6B,GACvC,SAAS,CAAC,yDAAyD,CAAC,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAE3F,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAElE,wBAAsB,sBAAsB,CAAC,EAC3C,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,IAAI,EACJ,SAAS,EACT,IAAI,EACJ,SAAS,GACV,EAAE,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAgBzB"} -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/review/_post.d.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type OpenAI from "openai"; 3 | export declare const querySchema: z.ZodObject<{ 4 | path: z.ZodString; 5 | content: z.ZodString; 6 | startLine: z.ZodDefault; 7 | rule: z.ZodObject<{ 8 | code: z.ZodString; 9 | level: z.ZodUnion<[z.ZodLiteral<"error">, z.ZodLiteral<"warn">]>; 10 | description: z.ZodString; 11 | }, "strip", z.ZodTypeAny, { 12 | code: string; 13 | level: "error" | "warn"; 14 | description: string; 15 | }, { 16 | code: string; 17 | level: "error" | "warn"; 18 | description: string; 19 | }>; 20 | model: z.ZodDefault, z.ZodLiteral<"basic">]>>; 21 | }, "strip", z.ZodTypeAny, { 22 | path: string; 23 | content: string; 24 | startLine: number; 25 | rule: { 26 | code: string; 27 | level: "error" | "warn"; 28 | description: string; 29 | }; 30 | model: "pro" | "basic"; 31 | }, { 32 | path: string; 33 | content: string; 34 | rule: { 35 | code: string; 36 | level: "error" | "warn"; 37 | description: string; 38 | }; 39 | startLine?: number | undefined; 40 | model?: "pro" | "basic" | undefined; 41 | }>; 42 | export type PostReviewResponse = { 43 | violations: { 44 | description: string | undefined; 45 | linesWithFix: string | undefined; 46 | linesWithoutFix: string; 47 | startLine: number; 48 | endLine: number; 49 | textToReplace: string; 50 | replaceTextWithFix: string; 51 | }[]; 52 | rule: { 53 | level: "error" | "warn" | undefined; 54 | code: string; 55 | description: string; 56 | }; 57 | path: string; 58 | cached: boolean; 59 | model: string; 60 | usage: OpenAI.Completions.CompletionUsage | undefined; 61 | } | undefined; 62 | export type PostReviewInput = z.input; 63 | export declare function createReview({ path, content, startLine, rule, model, }: PostReviewInput): Promise>; 64 | //# sourceMappingURL=_post.d.ts.map -------------------------------------------------------------------------------- /actions/review-action/dist/packages/requests/review/_post.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"_post.d.ts","sourceRoot":"","sources":["file:///Users/nicholashaley/floe/packages/requests/review/_post.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUtB,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAC1B;IACE,UAAU,EAAE;QAEV,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;QAEhC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;QAEjC,eAAe,EAAE,MAAM,CAAC;QAExB,SAAS,EAAE,MAAM,CAAC;QAElB,OAAO,EAAE,MAAM,CAAC;QAEhB,aAAa,EAAE,MAAM,CAAC;QAEtB,kBAAkB,EAAE,MAAM,CAAC;KAC5B,EAAE,CAAC;IACJ,IAAI,EAAE;QACJ,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;QACpC,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,WAAW,CAAC,eAAe,GAAG,SAAS,CAAC;CACvD,GACD,SAAS,CAAC;AACd,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAE1D,wBAAsB,YAAY,CAAC,EACjC,IAAI,EACJ,OAAO,EACP,SAAS,EACT,IAAI,EACJ,KAAK,GACN,EAAE,eAAe,mEAQjB"} -------------------------------------------------------------------------------- /actions/review-action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floe/review-action", 3 | "version": "0.1.0-beta.8", 4 | "private": true, 5 | "scripts": { 6 | "build": "ncc build src/index.ts --out dist --source-map --license licenses.txt", 7 | "dev": "pnpm build --watch", 8 | "lint": "eslint ." 9 | }, 10 | "dependencies": { 11 | "@actions/core": "^1.10.1", 12 | "@actions/github": "^6.0.0", 13 | "@floe/features": "workspace:*", 14 | "@floe/lib": "workspace:*", 15 | "@floe/requests": "workspace:*" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20.8.10", 19 | "@vercel/ncc": "^0.38.1", 20 | "eslint-config-custom": "workspace:*", 21 | "tsconfig": "workspace:*", 22 | "typescript": "^5.3.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /actions/review-action/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Inputs { 2 | token: string; 3 | } 4 | -------------------------------------------------------------------------------- /actions/review-action/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "~/*": ["./src/*"] 6 | } 7 | }, 8 | "include": ["src"], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/.env.example: -------------------------------------------------------------------------------- 1 | # Created by Vercel CLI 2 | APP_ID= 3 | DATABASE_URL="mysql://root@127.0.0.1:3309/?connection_limit=100" 4 | ENABLE_EXPERIMENTAL_COREPACK="1" 5 | FLOE_SECRET_IV= 6 | FLOE_SECRET_KEY= 7 | KV_REST_API_READ_ONLY_TOKEN= 8 | KV_REST_API_TOKEN= 9 | KV_REST_API_URL= 10 | KV_URL= 11 | OPENAI_API_KEY= 12 | PRIVATE_KEY= 13 | LANGFUSE_SECRET_KEY= 14 | LANGFUSE_PUBLIC_KEY= -------------------------------------------------------------------------------- /apps/api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/next"], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/api/.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # Sentry Auth Token 38 | .sentryclirc 39 | -------------------------------------------------------------------------------- /apps/api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @floe/api 2 | 3 | ## 0.1.0-beta.8 4 | 5 | ### Minor Changes 6 | 7 | - Add support for token usage and pro / basic models. 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @floe/db@0.1.0-beta.3 13 | - @floe/lib@0.1.0-beta.5 14 | - @floe/requests@0.1.0-beta.7 15 | 16 | ## 0.1.0-beta.7 17 | 18 | ### Minor Changes 19 | 20 | - 5f84851: Add support for token usage and pro / basic models. 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [5f84851] 25 | - @floe/requests@0.1.0-beta.6 26 | - @floe/lib@0.1.0-beta.4 27 | - @floe/db@0.1.0-beta.2 28 | 29 | ## 0.1.0-beta.6 30 | 31 | ### Patch Changes 32 | 33 | - Updated dependencies 34 | - @floe/requests@0.1.0-beta.5 35 | 36 | ## 0.1.0-beta.5 37 | 38 | ### Patch Changes 39 | 40 | - Updated dependencies [2cae03a] 41 | - @floe/requests@0.1.0-beta.4 42 | 43 | ## 0.1.0-beta.4 44 | 45 | ### Minor Changes 46 | 47 | - Stability fixes to reviews. 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies 52 | - @floe/requests@0.1.0-beta.3 53 | 54 | ## 0.1.0-beta.3 55 | 56 | ### Minor Changes 57 | 58 | - Bump to beta version. 59 | 60 | ### Patch Changes 61 | 62 | - Updated dependencies 63 | - @floe/db@0.1.0-beta.1 64 | - @floe/lib@0.1.0-beta.2 65 | - @floe/requests@0.1.0-beta.2 66 | 67 | ## 0.1.0-alpha.2 68 | 69 | ### Patch Changes 70 | 71 | - Updated dependencies 72 | - @floe/lib@0.1.0-alpha.1 73 | - @floe/requests@0.1.0-alpha.1 74 | 75 | ## 0.1.0-alpha.1 76 | 77 | ### Minor Changes 78 | 79 | - c8fa9fd: Improve error handling. 80 | 81 | ### Patch Changes 82 | 83 | - Updated dependencies [c8fa9fd] 84 | - @floe/lib@0.1.0-alpha.0 85 | 86 | ## 0.1.0-alpha.0 87 | 88 | ### Minor Changes 89 | 90 | - Create initial alpha version. 91 | 92 | ### Patch Changes 93 | 94 | - Updated dependencies 95 | - @floe/db@0.1.0-alpha.0 96 | - @floe/utils@0.1.0-alpha.0 97 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floe/api", 3 | "version": "0.1.0-beta.8", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 4000", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", 11 | "typecheck": "npx tsc --project ./tsconfig.json --noEmit" 12 | }, 13 | "dependencies": { 14 | "@floe/db": "workspace:*", 15 | "@floe/lib": "workspace:*", 16 | "@floe/requests": "workspace:*", 17 | "@octokit/auth-app": "^6.0.1", 18 | "@octokit/core": "^5.0.1", 19 | "@pinecone-database/pinecone": "^1.1.2", 20 | "@sentry/nextjs": "^7.66.0", 21 | "@types/node": "20.5.9", 22 | "@upstash/ratelimit": "^1.0.0", 23 | "@vercel/kv": "^1.0.1", 24 | "bcrypt": "^5.1.0", 25 | "handlebars": "^4.7.8", 26 | "js-tiktoken": "^1.0.7", 27 | "langfuse": "^2.2.0", 28 | "minimatch": "^9.0.3", 29 | "next": "^14.0.1", 30 | "next-api-middleware": "^2.0.1", 31 | "octokit": "^3.1.1", 32 | "openai": "^4.19.0", 33 | "qs": "^6.11.2", 34 | "request-ip": "^3.3.0", 35 | "typescript": "5.2.2", 36 | "zod": "^3.22.4", 37 | "zod-validation-error": "^2.1.0" 38 | }, 39 | "devDependencies": { 40 | "@floe/requests": "workspace:*", 41 | "@octokit/types": "^12.3.0", 42 | "@prisma/nextjs-monorepo-workaround-plugin": "^5.3.1", 43 | "@types/bcrypt": "^5.0.0", 44 | "@types/memory-cache": "^0.2.5", 45 | "@types/qs": "^6.9.10", 46 | "@types/request-ip": "^0.0.41", 47 | "eslint-config-custom": "workspace:*", 48 | "tsconfig": "workspace:*" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/api/sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://515e3e1b98c4c1c67ea35fe6e5cff9e0@o4505825864581120.ingest.sentry.io/4505825864712192", 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | 16 | replaysOnErrorSampleRate: 1.0, 17 | 18 | // This sets the sample rate to be 10%. You may want this to be 100% while 19 | // in development and sample at a lower rate in production 20 | replaysSessionSampleRate: 0.1, 21 | 22 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 23 | integrations: [ 24 | new Sentry.Replay({ 25 | // Additional Replay configuration goes in here, for example: 26 | maskAllText: true, 27 | blockAllMedia: true, 28 | }), 29 | ], 30 | }); 31 | -------------------------------------------------------------------------------- /apps/api/sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: "https://515e3e1b98c4c1c67ea35fe6e5cff9e0@o4505825864581120.ingest.sentry.io/4505825864712192", 10 | 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /apps/api/sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://515e3e1b98c4c1c67ea35fe6e5cff9e0@o4505825864581120.ingest.sentry.io/4505825864712192", 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /apps/api/src/constants/ignore-list.ts: -------------------------------------------------------------------------------- 1 | export const ignoreFiles = [ 2 | "package-lock.json", 3 | "yarn.lock", 4 | "Gemfile.lock", 5 | ".gitignore", 6 | "node_modules", 7 | "bower_components", 8 | "vendor", 9 | "composer.lock", 10 | ".DS_Store", 11 | ".vscode", 12 | ".idea", 13 | ".npmrc", 14 | ".yarnrc", 15 | "yarn-error.log", 16 | "yarn-debug.log", 17 | "error_log", 18 | "Thumbs.db", 19 | ".cache", 20 | "coverage", 21 | ".env", 22 | ".env.local", 23 | ".env.development", 24 | ".env.production", 25 | "npm-debug.log", 26 | "log.txt", 27 | ".log", 28 | "logs", 29 | "tmp", 30 | "build", 31 | "dist", 32 | ".git", 33 | "*.swp", 34 | "test-results.xml", 35 | "pnpm-lock.yaml", 36 | ]; 37 | -------------------------------------------------------------------------------- /apps/api/src/constants/supported-files.ts: -------------------------------------------------------------------------------- 1 | export const supportFiles = [".md", ".mdx", ".mdoc"]; 2 | -------------------------------------------------------------------------------- /apps/api/src/lib/github/compare.ts: -------------------------------------------------------------------------------- 1 | import type { Octokit } from "@octokit/core"; 2 | import { ignoreFiles } from "~/constants/ignore-list"; 3 | import type { CompareInfo } from "~/types/compare"; 4 | 5 | export async function getGitHubGitDiff( 6 | owner: string, 7 | repo: string, 8 | base: string, 9 | head: string, 10 | octokit: Octokit 11 | ): Promise { 12 | try { 13 | const compareInfo = await octokit.request( 14 | // could make this more open ended 15 | `GET /repos/{owner}/{repo}/compare/{base}...{head}`, 16 | { 17 | owner, 18 | repo, 19 | base, 20 | head, 21 | headers: { 22 | "X-GitHub-Api-Version": "2022-11-28", 23 | }, 24 | } 25 | ); 26 | 27 | const commits = compareInfo.data.commits.map((commit) => ({ 28 | sha: commit.sha, 29 | message: commit.commit.message, 30 | })); 31 | 32 | // Access the diff information 33 | const diffs = 34 | compareInfo.data.files?.reduce((acc, file) => { 35 | /** 36 | * Ignore file path if it's in the ignore list 37 | */ 38 | if ( 39 | ignoreFiles.some((ignoreFile) => file.filename.includes(ignoreFile)) 40 | ) { 41 | console.log("Ignoring file: ", file.filename); 42 | return acc; 43 | } 44 | 45 | return [ 46 | ...acc, 47 | { 48 | filename: file.filename, 49 | content: file.patch ?? "", 50 | isDeleted: file.status === "removed", 51 | contentsUrl: file.contents_url, 52 | }, 53 | ]; 54 | }, []) ?? []; 55 | 56 | return { 57 | commits, 58 | diffs, 59 | }; 60 | } catch (error) { 61 | console.error("Error:", error); 62 | return null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/api/src/lib/github/contents.ts: -------------------------------------------------------------------------------- 1 | import type { Octokit } from "@octokit/core"; 2 | import type { Endpoints } from "@octokit/types"; 3 | 4 | export async function getGitHubContents(contentsUrl: string, octokit: Octokit) { 5 | const auth = (await octokit.auth()) as { token: string }; 6 | 7 | const response = await fetch(contentsUrl, { 8 | headers: { 9 | Authorization: `token ${auth.token}`, 10 | "X-GitHub-Api-Version": "2022-11-28", 11 | Accept: "application/vnd.github+json", 12 | }, 13 | }); 14 | 15 | if (!response.ok) { 16 | throw new Error("Could not fetch GitHub contents"); 17 | } 18 | 19 | const jsonResponse = 20 | (await response.json()) as Endpoints["GET /repos/{owner}/{repo}/contents/{path}"]["response"]["data"]; 21 | 22 | // @ts-expect-error -- No need for complicated unwrap 23 | return Buffer.from(jsonResponse.content as string, "base64").toString( 24 | "utf-8" 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/api/src/lib/github/octokit.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "octokit"; 2 | import { createAppAuth } from "@octokit/auth-app"; 3 | import { HttpError } from "@floe/lib/http-error"; 4 | 5 | export const getOctokit = async (installationId: number) => { 6 | if (!process.env.APP_ID || !process.env.PRIVATE_KEY) { 7 | throw new Error("APP_ID or PRIVATE_KEY not set"); 8 | } 9 | 10 | /** 11 | * Generate JWT 12 | * See Step 1: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app#generating-an-installation-access-token 13 | */ 14 | try { 15 | const auth = createAppAuth({ 16 | appId: process.env.APP_ID, 17 | privateKey: process.env.PRIVATE_KEY, 18 | }); 19 | 20 | console.log("INSTALLATION ID: ", installationId); 21 | 22 | // Retrieve installation access token 23 | const installationAuthentication = await auth({ 24 | type: "installation", 25 | installationId, 26 | }); 27 | 28 | console.log("GITHUB TOKEN: ", installationAuthentication.token); 29 | 30 | return new Octokit({ 31 | auth: installationAuthentication.token, 32 | }); 33 | } catch (error) { 34 | console.log("ERROR: ", error.message); 35 | 36 | throw new HttpError({ 37 | message: 38 | "Could not authenticate with GitHub. Please make sure the GitHub App is installed.", 39 | statusCode: 403, 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /apps/api/src/lib/gitlab/compare.ts: -------------------------------------------------------------------------------- 1 | import { ignoreFiles } from "~/constants/ignore-list"; 2 | import type { CompareInfo } from "~/types/compare"; 3 | 4 | export async function getGitLabGitDiff( 5 | owner: string, 6 | repo: string, 7 | base: string, 8 | head: string, 9 | token: string 10 | ): Promise { 11 | try { 12 | const queryParams = new URLSearchParams({ 13 | from: base, 14 | to: head, 15 | straight: "false", 16 | }).toString(); 17 | 18 | const response = await fetch( 19 | `https://gitlab.com/api/v4/projects/${owner}%2F${repo}/repository/compare?${queryParams}`, 20 | { 21 | headers: { 22 | "PRIVATE-TOKEN": token, 23 | }, 24 | } 25 | ); 26 | 27 | if (!response.ok) { 28 | throw new Error(response.statusText); 29 | } 30 | 31 | const compareInfo = await response.json(); 32 | 33 | const commits = compareInfo.commits.map((commit) => ({ 34 | sha: commit.id, 35 | message: commit.title, 36 | })); 37 | 38 | // Access the diff information 39 | const diffs = 40 | compareInfo.diffs.reduce((acc, file) => { 41 | /** 42 | * Ignore file path if it's in the ignore list 43 | */ 44 | if ( 45 | ignoreFiles.some((ignoreFile) => file.new_path.includes(ignoreFile)) 46 | ) { 47 | console.log("Ignoring file: ", file.new_path); 48 | return acc; 49 | } 50 | 51 | return [ 52 | ...acc, 53 | { 54 | filename: file.new_path, 55 | content: file.diff ?? "", 56 | // TODO: Need to verify these 57 | isDeleted: file.deleted_file === "true", 58 | contentsUrl: "...", 59 | }, 60 | ]; 61 | }, []) ?? []; 62 | 63 | return { 64 | commits, 65 | diffs, 66 | }; 67 | } catch (error) { 68 | console.error("Error:", error); 69 | return null; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/ai-rate-limiter.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { Ratelimit } from "@upstash/ratelimit"; 3 | import { kv } from "@vercel/kv"; 4 | import type { CustomMiddleware } from "~/types/middleware"; 5 | 6 | const ratelimit = { 7 | // Free 8 | freeMinute: new Ratelimit({ 9 | redis: kv, 10 | analytics: true, 11 | prefix: "ai-ratelimit:free:minute", 12 | limiter: Ratelimit.slidingWindow(100, "60s"), // Temporarily increased to 100 during beta 13 | // limiter: Ratelimit.slidingWindow(5, "60s"), 14 | }), 15 | freeDay: new Ratelimit({ 16 | redis: kv, 17 | analytics: true, 18 | prefix: "ai-ratelimit:free:day", 19 | limiter: Ratelimit.slidingWindow(1000, "86400s"), // Temporarily increased to 1000 during beta 20 | // limiter: Ratelimit.slidingWindow(200, "86400s"), 21 | }), 22 | 23 | // Pro 24 | proMinute: new Ratelimit({ 25 | redis: kv, 26 | analytics: true, 27 | prefix: "ai-ratelimit:pro:minute", 28 | limiter: Ratelimit.slidingWindow(50, "60s"), 29 | }), 30 | proDay: new Ratelimit({ 31 | redis: kv, 32 | analytics: true, 33 | prefix: "ai-ratelimit:pro:day ", 34 | limiter: Ratelimit.slidingWindow(2000, "86400s"), 35 | }), 36 | 37 | // Team For team, just rely on the IP rate limiter. If this gets out of hand 38 | // other rate limiters can be 39 | }; 40 | 41 | export const aiRateLimiter: CustomMiddleware = async (req, res, next) => { 42 | const subscription = req.workspace.subscription; 43 | const hasProSubscription = 44 | subscription?.priceId === process.env.STRIPE_PRO_PRICE_ID; 45 | 46 | // Has a custom team tier, so can ignore these rate limits 47 | if (subscription && !hasProSubscription) { 48 | await next(); 49 | } 50 | 51 | const { success: successMinute } = subscription 52 | ? await ratelimit.proMinute.limit(req.workspace.id) 53 | : await ratelimit.freeMinute.limit(req.workspace.id); 54 | 55 | const { success: successDay } = subscription 56 | ? await ratelimit.proDay.limit(req.workspace.id) 57 | : await ratelimit.freeDay.limit(req.workspace.id); 58 | 59 | if (!successMinute || !successDay) { 60 | throw new HttpError({ 61 | statusCode: 429, 62 | message: "Too Many Requests.", 63 | }); 64 | } 65 | 66 | await next(); 67 | }; 68 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/api-id.ts: -------------------------------------------------------------------------------- 1 | import type { CustomMiddleware } from "~/types/middleware"; 2 | 3 | export const apiID: CustomMiddleware = async (req, res, next) => { 4 | const slug = req.headers["x-api-workspace"] as string | undefined; 5 | 6 | if (!slug) { 7 | res.status(401).json({ 8 | message: "No workspace slug provided", 9 | }); 10 | return; 11 | } 12 | 13 | await next(); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/authenticate.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | import { db } from "@floe/db"; 3 | import type { CustomMiddleware } from "~/types/middleware"; 4 | 5 | export const authenticate: CustomMiddleware = async (req, res, next) => { 6 | const slug = req.headers["x-api-workspace"] as string | undefined; 7 | const key = req.headers["x-api-key"] as string | undefined; 8 | 9 | if (!key) { 10 | res.status(401).json({ 11 | message: "No api key provided", 12 | }); 13 | return; 14 | } 15 | 16 | if (!slug) { 17 | res.status(401).json({ 18 | message: "No workspace slug provided", 19 | }); 20 | return; 21 | } 22 | 23 | const workspace = await db.workspace.findUnique({ 24 | where: { 25 | slug, 26 | }, 27 | include: { 28 | encrytpedKeys: { 29 | where: { 30 | slug: key.slice(-4), 31 | }, 32 | }, 33 | githubIntegration: true, 34 | gitlabIntegration: true, 35 | subscription: true, 36 | }, 37 | }); 38 | 39 | if (!workspace || !workspace.encrytpedKeys[0]) { 40 | res.status(401).json({ 41 | message: "Invalid API key", 42 | }); 43 | return; 44 | } 45 | 46 | const match = await bcrypt.compare(key, workspace.encrytpedKeys[0].key); 47 | 48 | if (!match) { 49 | res.status(401).json({ 50 | message: "Invalid API key", 51 | }); 52 | return; 53 | } 54 | 55 | req.workspace = workspace; 56 | req.workspaceSlug = slug; 57 | 58 | await next(); 59 | }; 60 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/capture-errors.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import * as Sentry from "@sentry/nextjs"; 3 | import type { Middleware } from "next-api-middleware"; 4 | 5 | export const captureErrors: Middleware = async (_req, res, next) => { 6 | try { 7 | // Catch any errors that are thrown in remaining 8 | // middleware and the API route handler 9 | await next(); 10 | } catch (error) { 11 | if (error instanceof HttpError) { 12 | res.status(error.statusCode).json({ message: error.message, error }); 13 | return; 14 | } 15 | 16 | /** 17 | * TODO: Handle other errors here for better error messages. 18 | * Eg. Zod errors 19 | */ 20 | 21 | /** 22 | * If we get here, it means that we have an unhandled error 23 | */ 24 | Sentry.captureException(error); 25 | 26 | res.status(500).json({ 27 | message: `Unhandled error of type '${typeof error}'. Please reach out for our customer support.`, 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/default-handler.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NextApiHandler, 3 | NextApiRequestExtension, 4 | NextApiResponseExtension, 5 | } from "~/types/middleware"; 6 | 7 | type Handlers = { 8 | [method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: { 9 | [key in number]: Promise<{ 10 | default: NextApiHandler; 11 | }>; 12 | }; 13 | }; 14 | 15 | export const defaultHandler = 16 | (handlers: Handlers) => 17 | async (req: NextApiRequestExtension, res: NextApiResponseExtension) => { 18 | if (req.method === "OPTIONS") { 19 | return res.status(200).end(); 20 | } 21 | 22 | const query = req.query as { version: string }; 23 | const versionNumber = parseInt(query.version, 10); 24 | 25 | if (isNaN(versionNumber)) { 26 | res.status(400).json({ 27 | message: "Invalid version", 28 | }); 29 | return; 30 | } 31 | 32 | const handler = ( 33 | await handlers[req.method as keyof typeof handlers]?.[versionNumber] 34 | )?.default; 35 | 36 | if (!handler) { 37 | res.status(405).json({ 38 | message: `Method Not Allowed (Allow: ${Object.keys(handlers).join( 39 | "," 40 | )})`, 41 | }); 42 | return; 43 | } 44 | 45 | await handler(req, res); 46 | }; 47 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/default-responder.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NextApiRequestExtension, 3 | NextApiResponseExtension, 4 | } from "~/types/middleware"; 5 | 6 | type Handle = ( 7 | req: NextApiRequestExtension, 8 | res: NextApiResponseExtension 9 | ) => Promise; 10 | 11 | export function defaultResponder(f: Handle) { 12 | return async ( 13 | req: NextApiRequestExtension, 14 | res: NextApiResponseExtension 15 | ) => { 16 | const result = (await f(req, res)) as unknown; 17 | if (result) res.json(result); 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/ip-rate-limiter.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit"; 2 | import { kv } from "@vercel/kv"; 3 | import { getClientIp } from "request-ip"; 4 | import type { Middleware } from "next-api-middleware"; 5 | 6 | const ratelimit = new Ratelimit({ 7 | redis: kv, 8 | analytics: true, 9 | prefix: "ip-ratelimit", 10 | limiter: Ratelimit.slidingWindow(100, "10s"), 11 | }); 12 | 13 | export const ipRateLimiter: Middleware = async (req, res, next) => { 14 | const ip = getClientIp(req); 15 | 16 | if (!ip) { 17 | await next(); 18 | return; 19 | } 20 | 21 | const { success } = await ratelimit.limit(ip); 22 | 23 | if (!success) { 24 | /** 25 | * By not throwing an error directly we avoid logging the error in Sentry 26 | */ 27 | res.status(429).json({ 28 | message: "Too Many Requests.", 29 | }); 30 | return; 31 | } 32 | 33 | await next(); 34 | }; 35 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/qs.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "qs"; 2 | import type { CustomMiddleware } from "~/types/middleware"; 3 | 4 | /** 5 | * This is used to parse the query string from the url instead of the built in req.query 6 | * req.query does not gracefully parce arrays or objects 7 | */ 8 | export const qs: CustomMiddleware = async (req, res, next) => { 9 | const url = req.url; 10 | 11 | if (!url) { 12 | return; 13 | } 14 | 15 | const queryString = url.split("?")[1]; 16 | 17 | // Parse the query object from the url 18 | const parsed = parse(queryString); 19 | 20 | req.queryObj = parsed; 21 | 22 | await next(); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/api/src/lib/middleware/with-middlware.ts: -------------------------------------------------------------------------------- 1 | import { label } from "next-api-middleware"; 2 | import { qs } from "./qs"; 3 | import { authenticate } from "./authenticate"; 4 | import { captureErrors } from "./capture-errors"; 5 | import { aiRateLimiter } from "./ai-rate-limiter"; 6 | import { ipRateLimiter } from "./ip-rate-limiter"; 7 | 8 | const withMiddleware = label( 9 | { 10 | qs, 11 | authenticate, 12 | captureErrors, 13 | aiRateLimiter, 14 | ipRateLimiter, 15 | }, 16 | // The order matters 17 | ["qs", "captureErrors", "ipRateLimiter"] 18 | ); 19 | 20 | export { withMiddleware }; 21 | -------------------------------------------------------------------------------- /apps/api/src/lib/normalizedGitProviders/compare.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { decryptData } from "@floe/lib/encryption"; 3 | import type { Workspace } from "~/types/middleware"; 4 | import type { CompareInfo } from "~/types/compare"; 5 | import { getGitHubGitDiff } from "../github/compare"; 6 | import { getOctokit } from "../github/octokit"; 7 | import { getGitLabGitDiff } from "../gitlab/compare"; 8 | 9 | /** 10 | * Fetch commits, diffs, etc for GitHub or GitLab 11 | */ 12 | export async function compare( 13 | parsed: { 14 | owner: string; 15 | repo: string; 16 | baseSha: string; 17 | headSha: string; 18 | }, 19 | workspace: Workspace 20 | ) { 21 | let compareInfo: CompareInfo | null = null; 22 | 23 | /** 24 | * Can only have githubIntegration or gitlabIntegration, not both 25 | */ 26 | if (workspace.githubIntegration) { 27 | if (!workspace.githubIntegration.installationId) { 28 | throw new HttpError({ 29 | statusCode: 400, 30 | message: "The GitHub integration is pending approval", 31 | }); 32 | } 33 | 34 | const octokit = await getOctokit( 35 | workspace.githubIntegration.installationId 36 | ); 37 | 38 | const diffResult = await getGitHubGitDiff( 39 | parsed.owner, 40 | parsed.repo, 41 | parsed.baseSha, 42 | parsed.headSha, 43 | octokit 44 | ); 45 | 46 | if (!diffResult) { 47 | return; 48 | } 49 | 50 | compareInfo = { 51 | commits: diffResult.commits, 52 | diffs: diffResult.diffs, 53 | }; 54 | } else if (workspace.gitlabIntegration) { 55 | const token = decryptData(workspace.gitlabIntegration.encryptedAccessToken); 56 | 57 | const response = await getGitLabGitDiff( 58 | parsed.owner, 59 | parsed.repo, 60 | parsed.baseSha, 61 | parsed.headSha, 62 | token 63 | ); 64 | 65 | if (!response) { 66 | return; 67 | } 68 | 69 | compareInfo = { 70 | commits: response.commits, 71 | diffs: response.diffs, 72 | }; 73 | } else { 74 | throw new HttpError({ 75 | statusCode: 400, 76 | message: "Workspace does not have a GitHub or GitLab integration", 77 | }); 78 | } 79 | 80 | return compareInfo; 81 | } 82 | -------------------------------------------------------------------------------- /apps/api/src/lib/normalizedGitProviders/content.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import type { Workspace } from "~/types/middleware"; 3 | import { getOctokit } from "../github/octokit"; 4 | import { getGitHubContents } from "../github/contents"; 5 | 6 | /** 7 | * Fetch commits, diffs, etc for GitHub or GitLab 8 | */ 9 | export async function contents(contentsUrl: string, workspace: Workspace) { 10 | /** 11 | * Can only have githubIntegration or gitlabIntegration, not both 12 | */ 13 | if (workspace.githubIntegration) { 14 | if (!workspace.githubIntegration.installationId) { 15 | throw new HttpError({ 16 | statusCode: 400, 17 | message: "The GitHub integration is pending approval", 18 | }); 19 | } 20 | 21 | const octokit = await getOctokit( 22 | workspace.githubIntegration.installationId 23 | ); 24 | 25 | const githubContents = await getGitHubContents(contentsUrl, octokit); 26 | 27 | return githubContents; 28 | } else if (workspace.gitlabIntegration) { 29 | // TODO: implement 30 | // const token = decryptData(workspace.gitlabIntegration.encryptedAccessToken); 31 | } else { 32 | throw new HttpError({ 33 | statusCode: 400, 34 | message: "Workspace does not have a GitHub or GitLab integration", 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/api/src/lib/normalizedGitProviders/strings.ts: -------------------------------------------------------------------------------- 1 | import type { CompareInfo } from "~/types/compare"; 2 | 3 | export function commitsToString(commits: CompareInfo["commits"]) { 4 | return commits.map((commit) => commit.message).join("\n"); 5 | } 6 | 7 | export function diffsToString(diffs: CompareInfo["diffs"]) { 8 | return diffs 9 | .map((file) => `filename: ${file.filename}\ndiff: ${file.content}`) 10 | .join("\n"); 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/ai-create-diff/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultHandler } from "~/lib/middleware/default-handler"; 2 | import { withMiddleware } from "~/lib/middleware/with-middlware"; 3 | 4 | export default withMiddleware()( 5 | defaultHandler({ 6 | GET: { 7 | 1: import("./_get"), 8 | }, 9 | }) 10 | ); 11 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/issue-comments/[comment_id]/_patch.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { 3 | querySchema, 4 | type PatchGitIssueCommentsResponse, 5 | } from "@floe/requests/git/issue-comments/[comment_id]/_patch"; 6 | import type { NextApiRequestExtension } from "~/types/middleware"; 7 | import { getOctokit } from "~/lib/github/octokit"; 8 | import { defaultResponder } from "~/lib/middleware/default-responder"; 9 | import { zParse } from "~/utils/z-parse"; 10 | 11 | async function handler({ 12 | query, 13 | body, 14 | workspace, 15 | }: NextApiRequestExtension): Promise { 16 | const parsed = zParse(querySchema, { 17 | body: body.body, 18 | repo: body.repo, 19 | owner: body.owner, 20 | commentId: query.comment_id, 21 | }); 22 | 23 | if (workspace.gitlabIntegration) { 24 | throw new HttpError({ 25 | message: "GitLab is not yet supported for this endpoint.", 26 | statusCode: 400, 27 | }); 28 | } 29 | 30 | if (!workspace.githubIntegration) { 31 | throw new HttpError({ 32 | message: "You must first setup your GitHub integration.", 33 | statusCode: 400, 34 | }); 35 | } 36 | 37 | if (!workspace.githubIntegration.installationId) { 38 | throw new HttpError({ 39 | message: "You must first setup your GitHub integration.", 40 | statusCode: 400, 41 | }); 42 | } 43 | 44 | const octokit = await getOctokit(workspace.githubIntegration.installationId); 45 | 46 | const comments = await octokit.rest.issues 47 | .updateComment({ 48 | body: parsed.body, 49 | repo: parsed.repo, 50 | owner: parsed.owner, 51 | comment_id: parsed.commentId, 52 | }) 53 | .catch(() => { 54 | throw new HttpError({ 55 | message: "Could not update comment on GitHub.", 56 | statusCode: 500, 57 | }); 58 | }); 59 | 60 | return comments.data; 61 | } 62 | 63 | export default defaultResponder(handler); 64 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/issue-comments/[comment_id]/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultHandler } from "~/lib/middleware/default-handler"; 2 | import { withMiddleware } from "~/lib/middleware/with-middlware"; 3 | 4 | export default withMiddleware()( 5 | defaultHandler({ 6 | PATCH: { 7 | 1: import("./_patch"), 8 | }, 9 | }) 10 | ); 11 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/issue-comments/_get.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { 3 | querySchema, 4 | type GetGitIssueCommentsResponse, 5 | } from "@floe/requests/git/issue-comments/_get"; 6 | import type { NextApiRequestExtension } from "~/types/middleware"; 7 | import { getOctokit } from "~/lib/github/octokit"; 8 | import { defaultResponder } from "~/lib/middleware/default-responder"; 9 | import { zParse } from "~/utils/z-parse"; 10 | 11 | async function handler({ 12 | queryObj, 13 | workspace, 14 | }: NextApiRequestExtension): Promise { 15 | const parsed = zParse(querySchema, queryObj); 16 | 17 | if (workspace.gitlabIntegration) { 18 | throw new HttpError({ 19 | message: "GitLab is not yet supported for this endpoint.", 20 | statusCode: 400, 21 | }); 22 | } 23 | 24 | if (!workspace.githubIntegration) { 25 | throw new HttpError({ 26 | message: "You must first setup your GitHub integration.", 27 | statusCode: 400, 28 | }); 29 | } 30 | 31 | if (!workspace.githubIntegration.installationId) { 32 | throw new HttpError({ 33 | message: "You must first setup your GitHub integration.", 34 | statusCode: 400, 35 | }); 36 | } 37 | 38 | const octokit = await getOctokit(workspace.githubIntegration.installationId); 39 | 40 | const comments = await octokit 41 | .paginate(octokit.rest.issues.listComments, { 42 | owner: parsed.owner, 43 | repo: parsed.repo, 44 | issue_number: parsed.issueNumber, 45 | }) 46 | .catch(() => { 47 | throw new HttpError({ 48 | message: "Could not fetch comments from GitHub.", 49 | statusCode: 500, 50 | }); 51 | }); 52 | 53 | return comments; 54 | } 55 | 56 | export default defaultResponder(handler); 57 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/issue-comments/_post.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { 3 | querySchema, 4 | type PostGitIssueCommentsResponse, 5 | } from "@floe/requests/git/issue-comments/_post"; 6 | import type { NextApiRequestExtension } from "~/types/middleware"; 7 | import { getOctokit } from "~/lib/github/octokit"; 8 | import { defaultResponder } from "~/lib/middleware/default-responder"; 9 | import { zParse } from "~/utils/z-parse"; 10 | 11 | async function handler({ 12 | body, 13 | workspace, 14 | }: NextApiRequestExtension): Promise { 15 | const parsed = zParse(querySchema, body as Record); 16 | 17 | if (workspace.gitlabIntegration) { 18 | throw new HttpError({ 19 | message: "GitLab is not yet supported for this endpoint.", 20 | statusCode: 400, 21 | }); 22 | } 23 | 24 | if (!workspace.githubIntegration) { 25 | throw new HttpError({ 26 | message: "You must first setup your GitHub integration.", 27 | statusCode: 400, 28 | }); 29 | } 30 | 31 | if (!workspace.githubIntegration.installationId) { 32 | throw new HttpError({ 33 | message: "The GitHub integration is pending approval.", 34 | statusCode: 400, 35 | }); 36 | } 37 | 38 | const octokit = await getOctokit(workspace.githubIntegration.installationId); 39 | 40 | const comment = await octokit.rest.issues 41 | .createComment({ 42 | body: parsed.body, 43 | repo: parsed.repo, 44 | owner: parsed.owner, 45 | issue_number: parsed.issueNumber, 46 | }) 47 | .catch((e) => { 48 | console.error(e.message); 49 | 50 | throw new HttpError({ 51 | message: "Could not create comment on GitHub.", 52 | statusCode: 500, 53 | }); 54 | }); 55 | 56 | return comment.data; 57 | } 58 | 59 | export default defaultResponder(handler); 60 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/issue-comments/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultHandler } from "~/lib/middleware/default-handler"; 2 | import { withMiddleware } from "~/lib/middleware/with-middlware"; 3 | 4 | export default withMiddleware("authenticate")( 5 | defaultHandler({ 6 | GET: { 7 | 1: import("./_get"), 8 | }, 9 | POST: { 10 | 1: import("./_post"), 11 | }, 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/review-comments/README.md: -------------------------------------------------------------------------------- 1 | We proxy requests to GitHub via /issue-comments so that we can authenticate as the app installation. This way comments will show up as the app installation instead of the user. 2 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/review-comments/_get.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { querySchema } from "@floe/requests/git/review-comments/_get"; 3 | import type { GetGitReviewCommentsResponse } from "@floe/requests/git/review-comments/_get"; 4 | import type { NextApiRequestExtension } from "~/types/middleware"; 5 | import { getOctokit } from "~/lib/github/octokit"; 6 | import { defaultResponder } from "~/lib/middleware/default-responder"; 7 | import { zParse } from "~/utils/z-parse"; 8 | 9 | async function handler({ 10 | queryObj, 11 | workspace, 12 | }: NextApiRequestExtension): Promise { 13 | const parsed = zParse(querySchema, queryObj); 14 | 15 | if (workspace.gitlabIntegration) { 16 | throw new HttpError({ 17 | message: "GitLab is not yet supported for this endpoint.", 18 | statusCode: 400, 19 | }); 20 | } 21 | 22 | if (!workspace.githubIntegration) { 23 | throw new HttpError({ 24 | message: "You must first setup your GitHub integration.", 25 | statusCode: 400, 26 | }); 27 | } 28 | 29 | if (!workspace.githubIntegration.installationId) { 30 | throw new HttpError({ 31 | message: "The GitHub integration is pending approval.", 32 | statusCode: 400, 33 | }); 34 | } 35 | 36 | const octokit = await getOctokit(workspace.githubIntegration.installationId); 37 | 38 | const comments = await octokit 39 | .paginate(octokit.rest.pulls.listReviewComments, { 40 | owner: parsed.owner, 41 | repo: parsed.repo, 42 | pull_number: parsed.pullNumber, 43 | }) 44 | .catch(() => { 45 | throw new HttpError({ 46 | message: "Could not fetch comments from GitHub.", 47 | statusCode: 500, 48 | }); 49 | }); 50 | 51 | return comments; 52 | } 53 | 54 | export default defaultResponder(handler); 55 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/review-comments/_post.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { querySchema } from "@floe/requests/git/review-comments/_post"; 3 | import type { PostGitReviewCommentsResponse } from "@floe/requests/git/review-comments/_post"; 4 | import type { NextApiRequestExtension } from "~/types/middleware"; 5 | import { getOctokit } from "~/lib/github/octokit"; 6 | import { defaultResponder } from "~/lib/middleware/default-responder"; 7 | import { zParse } from "~/utils/z-parse"; 8 | 9 | async function handler({ 10 | body, 11 | workspace, 12 | }: NextApiRequestExtension): Promise { 13 | const parsed = zParse(querySchema, body as Record); 14 | 15 | if (workspace.gitlabIntegration) { 16 | throw new HttpError({ 17 | message: "GitLab is not yet supported for this endpoint.", 18 | statusCode: 400, 19 | }); 20 | } 21 | 22 | if (!workspace.githubIntegration) { 23 | throw new HttpError({ 24 | message: "You must first setup your GitHub integration.", 25 | statusCode: 400, 26 | }); 27 | } 28 | 29 | if (!workspace.githubIntegration.installationId) { 30 | throw new HttpError({ 31 | message: "The GitHub integration is pending approval.", 32 | statusCode: 400, 33 | }); 34 | } 35 | 36 | const octokit = await getOctokit(workspace.githubIntegration.installationId); 37 | 38 | const comments = await octokit.rest.pulls 39 | .createReviewComment({ 40 | body: parsed.body, 41 | repo: parsed.repo, 42 | owner: parsed.owner, 43 | pull_number: parsed.pullNumber, 44 | line: parsed.line, 45 | start_line: parsed.startLine, 46 | side: parsed.side, 47 | start_side: parsed.startSide, 48 | path: parsed.path, 49 | commit_id: parsed.commitId, 50 | }) 51 | .catch((e) => { 52 | console.error(e.message); 53 | 54 | throw new HttpError({ 55 | message: "Could not create comment on GitHub.", 56 | statusCode: 500, 57 | }); 58 | }); 59 | 60 | return comments.data; 61 | } 62 | 63 | export default defaultResponder(handler); 64 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/git/review-comments/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultHandler } from "~/lib/middleware/default-handler"; 2 | import { withMiddleware } from "~/lib/middleware/with-middlware"; 3 | 4 | export default withMiddleware("authenticate")( 5 | defaultHandler({ 6 | GET: { 7 | 1: import("./_get"), 8 | }, 9 | POST: { 10 | 1: import("./_post"), 11 | }, 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/review/example.ts: -------------------------------------------------------------------------------- 1 | export const exampleContent = { 2 | "1": "These are my top 5 favourite movies of all time:", 3 | "2": "a. The Matrix", 4 | "3": "b. Babe: Pig in the City", 5 | "4": "c. Titanic", 6 | }; 7 | 8 | export const exampleRule = { 9 | code: "no-lettered-lists", 10 | level: "warn", 11 | description: "Do not use lettered lists. Use numbered lists instead.", 12 | }; 13 | 14 | export const exampleOutput = { 15 | violations: [ 16 | { 17 | description: "A lettered list is used. Use a numbered list instead.", 18 | startLine: 2, 19 | textToReplace: "a. The Matrix\nb. Babe: Pig in the City\nc. Titanic", 20 | replaceTextWithFix: "1. The Matrix\n2. Babe: Pig in the City\n3. Titanic", 21 | }, 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/review/index.ts: -------------------------------------------------------------------------------- 1 | import { defaultHandler } from "~/lib/middleware/default-handler"; 2 | import { withMiddleware } from "~/lib/middleware/with-middlware"; 3 | 4 | export default withMiddleware( 5 | "authenticate", 6 | "aiRateLimiter" 7 | )( 8 | defaultHandler({ 9 | POST: { 10 | 1: import("./_post"), 11 | }, 12 | }) 13 | ); 14 | -------------------------------------------------------------------------------- /apps/api/src/pages/api/review/prompts.ts: -------------------------------------------------------------------------------- 1 | import { handlebars } from "~/utils/handlebars"; 2 | 3 | export const systemInstructions = [ 4 | "Your job is to function as a writing assistant. You will be given CONTENT (an object where keys represent lineNumbers, and values represent content) and RULES (a dictionary). For every rule:", 5 | "1. Determine places where the rule is violated. You must only report on supplied rules. DO NOT add rules that have not been provided by the user.", 6 | "2. Describe why the violation was triggered in `description`.", 7 | "3. Report the `textToReplace` that should be replaced with the fix.", 8 | "4. Suggest a fix for the violation in `replaceTextWithFix`.", 9 | "5. Report the line number where the violation was triggered in `startLine`. This is the 'key' at the start of the line.", 10 | "Return a JSON response object with the following shape:", 11 | `{ 12 | "violations": [ 13 | { 14 | "description": "...", 15 | "startLine: "...", 16 | "textToReplace: "...", 17 | "replaceTextWithFix": "...", 18 | }, 19 | ... 20 | ] 21 | }`, 22 | ].join("\n"); 23 | 24 | export function getUserPrompt( 25 | content: Record, 26 | rule: { 27 | code: string; 28 | level: string; 29 | description: string; 30 | } 31 | ) { 32 | const promptTemplate = handlebars.compile( 33 | ` 34 | Please lint the following content based on the following rule: 35 | 36 | RULE: 37 | {{{rule}}} 38 | 39 | CONTENT: 40 | {{{content}}}` 41 | ); 42 | 43 | return promptTemplate({ 44 | rule: rule.description, 45 | content: JSON.stringify(content), 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /apps/api/src/types/compare.ts: -------------------------------------------------------------------------------- 1 | export interface CompareInfo { 2 | commits: { 3 | sha: string; 4 | message: string; 5 | }[]; 6 | diffs: { 7 | filename: string; 8 | content: string; 9 | isDeleted: boolean; 10 | contentsUrl: string; 11 | }[]; 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/src/types/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from "@floe/db"; 2 | import type { NextApiRequest, NextApiResponse, NextApiHandler } from "next"; 3 | import type { Middleware } from "next-api-middleware"; 4 | import type QueryString from "qs"; 5 | 6 | export type Workspace = Prisma.WorkspaceGetPayload<{ 7 | include: { 8 | encrytpedKeys: true; 9 | githubIntegration: true; 10 | gitlabIntegration: true; 11 | subscription: true; 12 | }; 13 | }>; 14 | 15 | export type NextApiRequestExtension = NextApiRequest & { 16 | workspace: Workspace; 17 | workspaceSlug: string; 18 | queryObj: QueryString.ParsedQs; 19 | }; 20 | 21 | export type NextApiResponseExtension = NextApiResponse; 22 | 23 | export type CustomMiddleware = Middleware; 24 | 25 | export type { NextApiHandler }; 26 | -------------------------------------------------------------------------------- /apps/api/src/utils/checksum.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "node:crypto"; 2 | 3 | export function createChecksum(data: string) { 4 | const hash = createHash("sha256"); 5 | hash.update(data); 6 | return hash.digest("base64"); 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/utils/get-cache-key.ts: -------------------------------------------------------------------------------- 1 | // FORMAT: `:::` 2 | 3 | export const getCacheKey = ( 4 | version: number, 5 | workspaceSlug: string, 6 | endpoint: string, 7 | hash: string 8 | ) => `${version}:${workspaceSlug}:${endpoint}:${hash}`; 9 | -------------------------------------------------------------------------------- /apps/api/src/utils/handlebars.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign -- TODO: refactor */ 2 | import handlebars from "handlebars"; 3 | 4 | handlebars.registerHelper("ai", (text: string) => { 5 | return `{{${text}}}`; 6 | }); 7 | 8 | function getVariablesFromStatementsRecursive(statements) { 9 | return statements.reduce((acc: string[], statement) => { 10 | const { type } = statement; 11 | 12 | if (type === "BlockStatement") { 13 | const { inverse, program } = statement; 14 | 15 | if (program?.body) { 16 | acc = acc.concat( 17 | getVariablesFromStatementsRecursive(program.body) as string 18 | ); 19 | } 20 | 21 | if (inverse?.body) { 22 | acc = acc.concat( 23 | getVariablesFromStatementsRecursive(inverse.body) as string 24 | ); 25 | } 26 | } else if (type === "MustacheStatement") { 27 | const { path } = statement; 28 | 29 | if (path?.original) { 30 | acc.push(path.original as string); 31 | } 32 | } 33 | 34 | return acc; 35 | }, []); 36 | } 37 | 38 | export function getHandlebarsVariables(input: string) { 39 | const ast = handlebars.parseWithoutProcessing(input); 40 | 41 | const rawVariables = getVariablesFromStatementsRecursive(ast.body); 42 | 43 | // Remove duplicates and "ai" 44 | return rawVariables.filter( 45 | (variable: string, index: number) => 46 | rawVariables.indexOf(variable) === index && variable !== "ai" 47 | ); 48 | } 49 | 50 | export { handlebars }; 51 | -------------------------------------------------------------------------------- /apps/api/src/utils/string-to-lines.ts: -------------------------------------------------------------------------------- 1 | export function stringToLines( 2 | string: string, 3 | startLine = 1 4 | ): Record { 5 | return string.split("\n").reduce((acc, line, index) => { 6 | return { 7 | ...acc, 8 | [`${index + startLine}`]: line, 9 | }; 10 | }, {}); 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/src/utils/z-parse.ts: -------------------------------------------------------------------------------- 1 | import { HttpError } from "@floe/lib/http-error"; 2 | import { ZodError } from "zod"; 3 | import type { z, AnyZodObject } from "zod"; 4 | import { fromZodError } from "zod-validation-error"; 5 | 6 | export function zParse( 7 | schema: T, 8 | query: Record 9 | ): z.infer { 10 | try { 11 | return schema.parse(query); 12 | } catch (error) { 13 | if (error instanceof ZodError) { 14 | throw new HttpError({ 15 | message: fromZodError(error).message, 16 | statusCode: 400, 17 | }); 18 | } 19 | throw new HttpError({ 20 | message: JSON.stringify(error), 21 | statusCode: 500, 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "downlevelIteration": true, 5 | "paths": { 6 | "~/*": ["./src/*"] 7 | } 8 | }, 9 | "include": [ 10 | ".eslintrc.js", 11 | "next-env.d.ts", 12 | "**/*.ts", 13 | "**/*.tsx", 14 | "**/*.cjs", 15 | "**/*.mjs", 16 | ".next/types/**/*.ts", 17 | ], 18 | "exclude": ["dist", "build", "node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/api/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "src/pages/api/**/*": { 4 | "maxDuration": 90 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/app/.env.example: -------------------------------------------------------------------------------- 1 | # Created by Vercel CLI 2 | DATABASE_URL="mysql://root@127.0.0.1:3309/?connection_limit=100" 3 | FLOE_SECRET_IV= 4 | FLOE_SECRET_KEY= 5 | GITHUB_CLIENT_ID= 6 | GITHUB_CLIENT_SECRET=x 7 | NEXTAUTH_SECRET= 8 | NEXTAUTH_URL= 9 | SENDGRID_API= 10 | STRIPE_PRO_PRICE_ID= 11 | STRIPE_SECRET_KEY= 12 | STRIPE_WEBHOOK_SECRET= 13 | -------------------------------------------------------------------------------- /apps/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["custom/next"], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/app/.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 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /apps/app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @floe/app 2 | 3 | ## 0.1.0-beta.5 4 | 5 | ### Minor Changes 6 | 7 | - Add support for token usage and pro / basic models. 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies 12 | - @floe/db@0.1.0-beta.3 13 | - @floe/lib@0.1.0-beta.5 14 | - @floe/ui@0.1.0-beta.4 15 | 16 | ## 0.1.0-beta.4 17 | 18 | ### Minor Changes 19 | 20 | - 5f84851: Add support for token usage and pro / basic models. 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [5f84851] 25 | - @floe/lib@0.1.0-beta.4 26 | - @floe/db@0.1.0-beta.2 27 | - @floe/ui@0.1.0-beta.3 28 | 29 | ## 0.1.0-beta.3 30 | 31 | ### Minor Changes 32 | 33 | - Bump to beta version. 34 | 35 | ### Patch Changes 36 | 37 | - Updated dependencies 38 | - @floe/db@0.1.0-beta.1 39 | - @floe/lib@0.1.0-beta.2 40 | - @floe/ui@0.1.0-beta.2 41 | 42 | ## 0.1.0-alpha.2 43 | 44 | ### Patch Changes 45 | 46 | - Updated dependencies 47 | - @floe/lib@0.1.0-alpha.1 48 | - @floe/ui@0.1.0-alpha.1 49 | 50 | ## 0.1.0-alpha.1 51 | 52 | ### Minor Changes 53 | 54 | - c8fa9fd: Improve error handling. 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies [c8fa9fd] 59 | - @floe/lib@0.1.0-alpha.0 60 | 61 | ## 0.1.0-alpha.0 62 | 63 | ### Minor Changes 64 | 65 | - Create initial alpha version. 66 | 67 | ### Patch Changes 68 | 69 | - Updated dependencies 70 | - @floe/db@0.1.0-alpha.0 71 | - @floe/ui@0.1.0-alpha.0 72 | - @floe/utils@0.1.0-alpha.0 73 | -------------------------------------------------------------------------------- /apps/app/README.md: -------------------------------------------------------------------------------- 1 | # Create T3 App 2 | 3 | This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. 4 | 5 | ## What's next? How do I make an app with this? 6 | 7 | We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. 8 | 9 | If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. 10 | 11 | - [Next.js](https://nextjs.org) 12 | - [NextAuth.js](https://next-auth.js.org) 13 | - [Prisma](https://prisma.io) 14 | - [Tailwind CSS](https://tailwindcss.com) 15 | - [tRPC](https://trpc.io) 16 | 17 | ## Learn More 18 | 19 | To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: 20 | 21 | - [Documentation](https://create.t3.gg/) 22 | - [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials 23 | 24 | You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! 25 | 26 | ## How do I deploy this? 27 | 28 | Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel), [Netlify](https://create.t3.gg/en/deployment/netlify) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. 29 | -------------------------------------------------------------------------------- /apps/app/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { PrismaPlugin } from "@prisma/nextjs-monorepo-workaround-plugin"; 2 | /** 3 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 4 | * for Docker builds. 5 | */ 6 | await import("./src/env.mjs"); 7 | 8 | /** @type {import("next").NextConfig} */ 9 | const nextConfig = { 10 | webpack: (config, { isServer }) => { 11 | if (isServer) { 12 | config.plugins = [...config.plugins, new PrismaPlugin()]; 13 | } 14 | 15 | return config; 16 | }, 17 | 18 | transpilePackages: ["@floe/ui"], 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /apps/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@floe/app", 3 | "version": "0.1.0-beta.5", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev -p 3001", 8 | "lint": "next lint", 9 | "start": "next start", 10 | "stripe:listen": "stripe listen --forward-to localhost:3001/api/webhooks/stripe", 11 | "stripe:trigger": "stripe trigger payment_intent.succeeded", 12 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next" 13 | }, 14 | "dependencies": { 15 | "@floe/db": "workspace:*", 16 | "@floe/lib": "workspace:*", 17 | "@floe/ui": "workspace:*", 18 | "@headlessui/react": "^1.7.16", 19 | "@heroicons/react": "^2.0.18", 20 | "@hookform/resolvers": "^3.3.2", 21 | "@next-auth/prisma-adapter": "^1.0.7", 22 | "@react-email/components": "^0.0.10", 23 | "@stripe/stripe-js": "^2.2.2", 24 | "@t3-oss/env-nextjs": "^0.7.0", 25 | "@tanstack/react-query": "^4.32.6", 26 | "@tremor/react": "^3.13.1", 27 | "bcrypt": "^5.1.1", 28 | "next": "^14.0.1", 29 | "next-auth": "^4.23.0", 30 | "nodemailer": "^6.9.7", 31 | "octokit": "^3.1.1", 32 | "react": "18.2.0", 33 | "react-dom": "18.2.0", 34 | "react-email": "^1.9.5", 35 | "stripe": "^14.10.0", 36 | "superjson": "^1.13.1", 37 | "zod": "^3.22.4" 38 | }, 39 | "devDependencies": { 40 | "@floe/tailwind": "workspace:*", 41 | "@prisma/nextjs-monorepo-workaround-plugin": "^5.3.1", 42 | "@types/bcrypt": "^5.0.1", 43 | "@types/eslint": "^8.44.2", 44 | "@types/node": "^18.16.0", 45 | "@types/react": "^18.2.20", 46 | "@types/react-dom": "^18.2.7", 47 | "@typescript-eslint/eslint-plugin": "^6.3.0", 48 | "@typescript-eslint/parser": "^6.3.0", 49 | "eslint": "^8.47.0", 50 | "eslint-config-custom": "workspace:*", 51 | "prisma": "^5.1.1", 52 | "react-hook-form": "^7.47.0", 53 | "tailwindcss": "^3.3.3", 54 | "tsconfig": "workspace:*", 55 | "typescript": "^5.1.6" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /apps/app/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | 4 | module.exports = { 5 | plugins: { 6 | tailwindcss: {}, 7 | autoprefixer: {}, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/app/public/favicon.ico -------------------------------------------------------------------------------- /apps/app/public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/app/public/github.png -------------------------------------------------------------------------------- /apps/app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/app/public/logo.png -------------------------------------------------------------------------------- /apps/app/src/app/(authenticated)/[workspace]/billing/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import { createOrRetrieveCustomer, stripe } from "~/lib/stripe"; 5 | import { getURL } from "~/utils/url"; 6 | 7 | const url = getURL(); 8 | 9 | export async function createStripeCheckoutSession( 10 | slug: string, 11 | priceId: string 12 | ) { 13 | // Retrieve or create the customer in Stripe 14 | const customer = await createOrRetrieveCustomer({ 15 | workspaceSlug: slug, 16 | }); 17 | 18 | const result = await stripe.checkout.sessions.create({ 19 | payment_method_types: ["card"], 20 | customer, 21 | customer_update: { 22 | address: "auto", 23 | }, 24 | line_items: [ 25 | { 26 | price: priceId, 27 | quantity: 1, 28 | }, 29 | ], 30 | mode: "subscription", 31 | allow_promotion_codes: true, 32 | success_url: `${url}${slug}/billing?success=true`, 33 | cancel_url: `${url}${slug}/billing?canceled=true`, 34 | }); 35 | 36 | if (!result.url) { 37 | throw new Error("No URL returned from Stripe"); 38 | } 39 | 40 | redirect(result.url); 41 | } 42 | 43 | export async function createPortalLink(slug: string) { 44 | const customer = await createOrRetrieveCustomer({ 45 | workspaceSlug: slug, 46 | }); 47 | 48 | const result = await stripe.billingPortal.sessions.create({ 49 | customer, 50 | return_url: `${url}${slug}/billing`, 51 | }); 52 | 53 | redirect(result.url); 54 | } 55 | -------------------------------------------------------------------------------- /apps/app/src/app/(authenticated)/[workspace]/developers/keys/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { z } from "zod"; 4 | import bcrypt from "bcrypt"; 5 | import { revalidatePath } from "next/cache"; 6 | import { db } from "@floe/db"; 7 | 8 | const schema = z.object({ 9 | name: z.string().min(3).max(24), 10 | workspaceId: z.string(), 11 | }); 12 | 13 | export const rollKey = async (name: string, workspaceId: string) => { 14 | const parsed = schema.parse({ 15 | name, 16 | workspaceId, 17 | }); 18 | 19 | const rounds = 10; 20 | // Use the user id as the primrary key 21 | const token = `secret_${crypto.randomUUID()}`; 22 | // Slug is the last 4 characters of the token 23 | const slug = token.slice(-4); 24 | 25 | await new Promise((resolve) => { 26 | bcrypt.hash(token, rounds, async (err, hash) => { 27 | if (err) { 28 | throw err; 29 | } 30 | 31 | await db.workspace.update({ 32 | where: { 33 | id: parsed.workspaceId, 34 | }, 35 | data: { 36 | encrytpedKeys: { 37 | createMany: { 38 | data: [ 39 | { 40 | name, 41 | key: hash, 42 | slug, 43 | }, 44 | ], 45 | }, 46 | }, 47 | }, 48 | }); 49 | 50 | resolve(null); 51 | }); 52 | }); 53 | 54 | revalidatePath(`/${workspaceId}/developers`); 55 | 56 | return token; 57 | }; 58 | 59 | export const deleteKey = async (slug: string, workspaceId: string) => { 60 | await db.encryptedKey.delete({ 61 | where: { 62 | workspaceId_slug: { 63 | slug, 64 | workspaceId, 65 | }, 66 | }, 67 | }); 68 | 69 | revalidatePath(`/${workspaceId}/developers`); 70 | }; 71 | -------------------------------------------------------------------------------- /apps/app/src/app/(authenticated)/[workspace]/developers/keys/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { ActionCard } from "@floe/ui"; 4 | import type { Prisma } from "@floe/db"; 5 | import { KeyModal } from "./key-modal"; 6 | import { Table } from "./table"; 7 | 8 | interface KeyProps { 9 | workspace: Prisma.WorkspaceGetPayload<{ 10 | include: { 11 | encrytpedKeys: { 12 | select: { 13 | name: true; 14 | slug: true; 15 | createdAt: true; 16 | }; 17 | }; 18 | }; 19 | }>; 20 | } 21 | 22 | function Keys({ workspace }: KeyProps) { 23 | const [open, setOpen] = useState(false); 24 | 25 | return ( 26 |
27 | { 32 | setOpen(true); 33 | }, 34 | }, 35 | ]} 36 | subtitle="API keys allow you to authenticate with the API." 37 | title="API keys" 38 | > 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | export default Keys; 47 | -------------------------------------------------------------------------------- /apps/app/src/app/(authenticated)/[workspace]/developers/page.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "~/app/_components/header"; 2 | import { getWorkspace } from "~/lib/features/workspace"; 3 | import Keys from "./keys"; 4 | 5 | export default async function Developers({ 6 | params, 7 | }: { 8 | params: { workspace: string }; 9 | }) { 10 | const workspace = await getWorkspace(params.workspace); 11 | 12 | if (!workspace) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
18 |
22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/app/src/app/(authenticated)/[workspace]/integrations/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@floe/db"; 4 | import { encryptData } from "@floe/lib/encryption"; 5 | 6 | export const setGitlabToken = (workspaceId: string, formData: FormData) => { 7 | const token = formData.get("token") as string; 8 | const encryptedToken = encryptData(token); 9 | 10 | return db.gitlabIntegration.upsert({ 11 | where: { 12 | workspaceId, 13 | }, 14 | create: { 15 | workspaceId, 16 | encryptedAccessToken: encryptedToken, 17 | }, 18 | update: { 19 | encryptedAccessToken: encryptedToken, 20 | }, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/app/src/app/(authenticated)/[workspace]/integrations/github-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@floe/ui"; 4 | import type { Prisma } from "@floe/db"; 5 | import { useGitHubInstallationURL } from "~/lib/features/github-installation"; 6 | 7 | interface GitHubButtonProps { 8 | workspace: Prisma.WorkspaceGetPayload<{ 9 | include: { 10 | githubIntegration: true; 11 | gitlabIntegration: true; 12 | encrytpedKeys: { 13 | select: { 14 | name: true; 15 | slug: true; 16 | createdAt: true; 17 | }; 18 | }; 19 | }; 20 | }>; 21 | } 22 | 23 | export function GitHubButton({ workspace }: GitHubButtonProps) { 24 | const installationUrl = useGitHubInstallationURL( 25 | workspace.id, 26 | workspace.slug 27 | ); 28 | 29 | if (!installationUrl) { 30 | return null; 31 | } 32 | 33 | return ( 34 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /apps/app/src/app/(authenticated)/[workspace]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth/next"; 2 | import { db } from "@floe/db"; 3 | import { authOptions } from "~/server/auth"; 4 | import { Nav } from "./nav"; 5 | 6 | async function getUser() { 7 | const session = await getServerSession(authOptions); 8 | 9 | if (!session) { 10 | return null; 11 | } 12 | 13 | return db.user.findUnique({ 14 | where: { id: session.user.id }, 15 | include: { 16 | workspaceMemberships: { 17 | include: { 18 | workspace: true, 19 | }, 20 | }, 21 | }, 22 | }); 23 | } 24 | 25 | export default async function WorkspaceLayout({ 26 | children, 27 | params, 28 | }: { 29 | children: React.ReactNode; 30 | params: { workspace: string }; 31 | }) { 32 | const user = await getUser(); 33 | 34 | return ( 35 | <> 36 |