├── .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 |
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 |
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 |
37 |
38 | {children}
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/apps/app/src/app/(authenticated)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth";
2 | import { redirect } from "next/navigation";
3 | import { authOptions } from "~/server/auth";
4 |
5 | export default async function RootLayout({ children }) {
6 | const session = await getServerSession(authOptions);
7 |
8 | /**
9 | * Using next-auth https://next-auth.js.org/configuration/nextjs#basic-usage
10 | * wasn't working. This works well though.
11 | */
12 | if (!session) {
13 | return redirect("/signin");
14 | }
15 |
16 | return <>{children}>;
17 | }
18 |
--------------------------------------------------------------------------------
/apps/app/src/app/(authenticated)/new/context.tsx:
--------------------------------------------------------------------------------
1 | import type { Prisma } from "@floe/db";
2 | import { createContext, useContext } from "react";
3 |
4 | export interface DefaultContext {
5 | step: number;
6 | setSearchParams: (
7 | obj: Record
8 | ) => void;
9 | workspace: Prisma.WorkspaceGetPayload<{
10 | include: {
11 | githubIntegration: true;
12 | gitlabIntegration: true;
13 | };
14 | }> | null;
15 | workspaceLoading: boolean;
16 | }
17 |
18 | export const StepsContext = createContext({} as DefaultContext);
19 | export const useStepsContext = () => useContext(StepsContext);
20 |
--------------------------------------------------------------------------------
/apps/app/src/app/(authenticated)/new/nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import logo from "public/logo.png";
5 | import { signOut } from "next-auth/react";
6 |
7 | export function Nav() {
8 | return (
9 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/apps/app/src/app/(authenticated)/page.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth";
2 | import { db } from "@floe/db";
3 | import { redirect } from "next/navigation";
4 | import { authOptions } from "~/server/auth";
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 Root() {
26 | const user = await getUser();
27 |
28 | if (!user?.workspaceMemberships.length) {
29 | redirect("/new");
30 | }
31 |
32 | redirect(`/${user.workspaceMemberships[0].workspace.slug}`);
33 | }
34 |
--------------------------------------------------------------------------------
/apps/app/src/app/_components/header.tsx:
--------------------------------------------------------------------------------
1 | interface HeaderProps {
2 | title: string;
3 | description: string;
4 | }
5 |
6 | export function Header({ title, description }: HeaderProps) {
7 | return (
8 |
9 |
{title}
10 |
{description}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/apps/app/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { authOptions } from "~/server/auth";
3 |
4 | const handler = NextAuth(authOptions);
5 | export { handler as GET, handler as POST };
6 |
--------------------------------------------------------------------------------
/apps/app/src/app/api/installation-callback/get-octokit.ts:
--------------------------------------------------------------------------------
1 | import { Octokit } from "octokit";
2 | import { env } from "~/env.mjs";
3 |
4 | export async function getOctokit(code: string) {
5 | const queryParams = new URLSearchParams({
6 | code,
7 | client_id: env.GITHUB_CLIENT_ID,
8 | client_secret: env.GITHUB_CLIENT_SECRET,
9 | }).toString();
10 |
11 | /**
12 | * Exchange the code for an access token.
13 | */
14 | const resp = await fetch(
15 | `https://github.com/login/oauth/access_token?${queryParams}`,
16 | {
17 | method: "POST",
18 | headers: {
19 | Accept: "application/json",
20 | },
21 | }
22 | );
23 |
24 | if (!resp.ok) {
25 | console.log(
26 | "Installation error. Failed to get access token with: ",
27 | resp.status,
28 | resp.statusText
29 | );
30 | // redirect(`${url}?installation_error=1`);
31 | throw new Error("Failed to get access token");
32 | }
33 |
34 | const { access_token: auth } = await resp.json();
35 |
36 | return new Octokit({
37 | auth,
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/apps/app/src/app/api/installation-callback/handle-setup-request-with-state.ts:
--------------------------------------------------------------------------------
1 | import type { z } from "zod";
2 | import { db } from "@floe/db";
3 | import { redirect } from "next/navigation";
4 | import { getServerSession } from "next-auth";
5 | import { HttpError } from "@floe/lib/http-error";
6 | import { parseGitHubInstallationCallback } from "~/lib/features/github-installation";
7 | import { authOptions } from "~/server/auth";
8 | import type { schema } from "./schema";
9 |
10 | export async function handleSetupRequestWithState(
11 | parsedSchema: z.infer
12 | ) {
13 | const { state } = parsedSchema;
14 | const session = await getServerSession(authOptions);
15 |
16 | if (!session) {
17 | throw new HttpError({
18 | message: "Unauthorized",
19 | statusCode: 401,
20 | });
21 | }
22 |
23 | if (!state) {
24 | throw new HttpError({
25 | message: "Invalid state",
26 | statusCode: 400,
27 | });
28 | }
29 |
30 | const { id, slug, path, url } = parseGitHubInstallationCallback(state);
31 |
32 | try {
33 | if (!path || !slug || !id) {
34 | throw new HttpError({
35 | message: "Invalid state",
36 | statusCode: 400,
37 | });
38 | }
39 |
40 | const workspace = await db.workspace.findUnique({
41 | where: {
42 | id,
43 | members: {
44 | some: {
45 | userId: session.user.id,
46 | },
47 | },
48 | },
49 | });
50 |
51 | if (!workspace) {
52 | throw new HttpError({
53 | message: "Unauthorized",
54 | statusCode: 401,
55 | });
56 | }
57 |
58 | /**
59 | * Create githubIntegration record
60 | */
61 | await db.githubIntegration.create({
62 | data: {
63 | workspaceId: id,
64 | },
65 | });
66 | } catch (e) {
67 | console.log("Installation error. Failed to update workspace with: ", e);
68 | redirect(`${url}?installation_error=1`);
69 | }
70 |
71 | redirect(url);
72 | }
73 |
--------------------------------------------------------------------------------
/apps/app/src/app/api/installation-callback/handle-setup-request-without-state.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | export function handleSetupRequestWithoutState() {
4 | redirect("/installation/requested");
5 | }
6 |
--------------------------------------------------------------------------------
/apps/app/src/app/api/installation-callback/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const schema = z.object({
4 | code: z.string(),
5 | state: z.string().nullish(),
6 | installationId: z.coerce.number().nullish(),
7 | setupAction: z.enum(["install", "request"]),
8 | });
9 |
--------------------------------------------------------------------------------
/apps/app/src/app/installation/confirmed/page.tsx:
--------------------------------------------------------------------------------
1 | import { CheckCircleIcon } from "@heroicons/react/24/solid";
2 |
3 | export default function Page() {
4 | return (
5 |
6 |
7 |
8 | Please contact us at{" "}
9 |
10 | contact@floe.dev
11 | {" "}
12 | to finalize your installation. Additionally, please include the
13 | following information:
14 |
15 |
16 | - Your Floe Workspace name
17 | - Your GitHub Org name
18 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/app/src/app/installation/requested/page.tsx:
--------------------------------------------------------------------------------
1 | import { CheckCircleIcon } from "@heroicons/react/24/solid";
2 |
3 | export default function Page() {
4 | return (
5 |
6 |
7 |
Your installation request has been sent to your organization admin.
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "~/styles/globals.css";
2 |
3 | import { Inter } from "next/font/google";
4 |
5 | const inter = Inter({
6 | subsets: ["latin"],
7 | });
8 |
9 | export const metadata = {
10 | title: "Floe dashboard",
11 | description: "Floe AI writing assistants",
12 | icons: [{ rel: "icon", url: "/favicon.ico" }],
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | }: {
18 | children: React.ReactNode;
19 | }): JSX.Element {
20 | return (
21 |
22 | {children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/apps/app/src/app/signin/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useState } from "react";
3 | import { signIn } from "next-auth/react";
4 | import { Button, Input, Spinner } from "@floe/ui";
5 | import Image from "next/image";
6 | import type { FormEvent } from "react";
7 | import { useSearchParams } from "next/navigation";
8 | import logo from "public/logo.png";
9 |
10 | function Form() {
11 | const searchParams = useSearchParams();
12 | const [loading, setLoading] = useState(false);
13 | const callbackUrl = searchParams.get("callbackUrl") || "/";
14 |
15 | async function handleSubmit(event: FormEvent) {
16 | event.preventDefault();
17 |
18 | if (loading) {
19 | return;
20 | }
21 |
22 | const email = event.currentTarget.email.value;
23 | setLoading(true);
24 | await signIn("sendgrid", { email, callbackUrl }).finally(() => {
25 | setLoading(false);
26 | });
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 |
Welcome
37 |
Please enter your company email to continue.
38 |
39 |
55 |
56 |
57 | );
58 | }
59 |
60 | export default Form;
61 |
--------------------------------------------------------------------------------
/apps/app/src/app/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next";
2 | import { redirect } from "next/navigation";
3 | import { authOptions } from "~/server/auth";
4 | import Form from "./form";
5 |
6 | export default async function Page() {
7 | const session = await getServerSession(authOptions);
8 |
9 | if (session) {
10 | return redirect("/");
11 | }
12 |
13 | return (
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/app/src/app/verify-request/page.tsx:
--------------------------------------------------------------------------------
1 | import { SparklesIcon } from "@heroicons/react/24/solid";
2 |
3 | export default function Page() {
4 | return (
5 |
6 |
7 |
A magic link has been sent to your email.
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/lib/features/github-installation.ts:
--------------------------------------------------------------------------------
1 | import { usePathname, useSearchParams } from "next/navigation";
2 |
3 | export function useGitHubInstallationURL(
4 | workspaceId: string | undefined,
5 | workspaceSlug: string | undefined
6 | ) {
7 | const pathname = usePathname();
8 | const searchParams = useSearchParams();
9 | const arr: string[] = [];
10 |
11 | if (!workspaceId || !workspaceSlug) {
12 | return null;
13 | }
14 |
15 | searchParams.forEach((val, key) => {
16 | arr.push(key, val);
17 | });
18 |
19 | const valuesToInclude = [workspaceId, workspaceSlug, pathname, ...arr];
20 | const encodedState = encodeURIComponent(valuesToInclude.join(","));
21 | const app =
22 | process.env.NODE_ENV === "production" ? "floe-app" : "floe-app-tester";
23 |
24 | return `https://github.com/apps/${app}/installations/new?state=${encodedState}`;
25 | }
26 |
27 | export function parseGitHubInstallationCallback(state: string) {
28 | const [id, slug, path, ...params] = state
29 | .split(",")
30 | .map((value) => decodeURIComponent(value));
31 |
32 | // In the params array, even numbers are keys, while odd numbers are values.
33 | // Convert to a query string.
34 | const queryString = params
35 | .reduce((acc, cur, i) => {
36 | // If even
37 | if (i % 2 === 0) {
38 | return [...acc, `${cur}=${params[i + 1]}`];
39 | }
40 | return acc;
41 | }, [])
42 | .join("&");
43 |
44 | const url = `${path}${queryString.length ? "?" : ""}${queryString}`;
45 |
46 | return {
47 | id,
48 | slug,
49 | path,
50 | url,
51 | };
52 | }
53 |
--------------------------------------------------------------------------------
/apps/app/src/lib/features/workspace.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@floe/db";
2 | import { getServerSession } from "next-auth";
3 | import { authOptions } from "~/server/auth";
4 |
5 | export async function getWorkspace(workspaceSlug: string) {
6 | const session = await getServerSession(authOptions);
7 |
8 | if (!session) {
9 | return null;
10 | }
11 |
12 | return db.workspace.findUnique({
13 | where: {
14 | slug: workspaceSlug,
15 | members: {
16 | some: {
17 | userId: session.user.id,
18 | },
19 | },
20 | },
21 | include: {
22 | githubIntegration: true,
23 | gitlabIntegration: true,
24 | encrytpedKeys: {
25 | select: {
26 | name: true,
27 | slug: true,
28 | createdAt: true,
29 | },
30 | },
31 | },
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/apps/app/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
--------------------------------------------------------------------------------
/apps/app/src/utils/url.ts:
--------------------------------------------------------------------------------
1 | import { env } from "~/env.mjs";
2 |
3 | export const getURL = () => {
4 | let url =
5 | env.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env.
6 | env.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel.
7 | "http://localhost:3001/";
8 | // Make sure to include `https://` when not localhost.
9 | url = url.includes("http") ? url : `https://${url}`;
10 | // Make sure to including trailing `/`.
11 | url = url.endsWith("/") ? url : `${url}/`;
12 |
13 | return url;
14 | };
15 |
--------------------------------------------------------------------------------
/apps/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/nextjs.json",
3 | "compilerOptions": {
4 | "paths": {
5 | "~/*": ["./src/*"]
6 | }
7 | },
8 | "include": [
9 | ".eslintrc.js",
10 | "next-env.d.ts",
11 | "**/*.ts",
12 | "**/*.tsx",
13 | "**/*.cjs",
14 | "**/*.mjs",
15 | ".next/types/**/*.ts",
16 | ],
17 | "exclude": ["dist", "build", "node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/next"],
3 | };
4 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | .vercel
2 |
3 | # next-video
4 | videos/*
5 | !videos/*.json
6 | !videos/*.js
7 | !videos/*.ts
8 | public/_next-video
9 |
--------------------------------------------------------------------------------
/apps/web/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/docs
2 |
3 | ## 0.1.0-beta.3
4 |
5 | ### Patch Changes
6 |
7 | - Updated dependencies
8 | - @floe/ui@0.1.0-beta.4
9 |
10 | ## 0.1.0-beta.2
11 |
12 | ### Patch Changes
13 |
14 | - Updated dependencies [5f84851]
15 | - @floe/ui@0.1.0-beta.3
16 |
17 | ## 0.1.0-beta.1
18 |
19 | ### Minor Changes
20 |
21 | - Bump to beta version.
22 |
23 | ## 0.1.0-alpha.0
24 |
25 | ### Minor Changes
26 |
27 | - Create initial alpha version.
28 |
--------------------------------------------------------------------------------
/apps/web/client.webpack.lock:
--------------------------------------------------------------------------------
1 | {
2 | "https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/rule-create-high-fps-xr0vXRV66mOUts5LGbSuiPGLgI1hXb.mp4": { "integrity": "sha512-jzJ13DqwevQJ+FxUJQs/A3fNzJwsKu4p9t2dxrRmSYmg7CZb/qbawvpugPx/zLJDkzzz38Cb2uaGAm41ouixjg==", "contentType": "video/mp4" },
3 | "https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/rule-review-voO0xrHQCQNMLosKi0dYiOW1GtX3ii.mp4": { "integrity": "sha512-J3efAE00munIW+Sqiy84IVw0Q+4FGz9LEy96dWXHoMz8COD9cj8fGNxWiIjxe2x8arIuRsOnc7+0nFRasQvoWw==", "contentType": "video/mp4" },
4 | "version": 1
5 | }
6 |
--------------------------------------------------------------------------------
/apps/web/components/authors.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import AvatarNic from "public/avatar-nic.jpeg";
3 |
4 | const AUTHORS = {
5 | nic: {
6 | name: "Nic Haley",
7 | avatar: AvatarNic,
8 | },
9 | };
10 |
11 | interface AuthorsProps {
12 | authors: (keyof typeof AUTHORS)[];
13 | }
14 |
15 | export function Authors({ authors }: AuthorsProps) {
16 | return (
17 |
18 |
Authors
19 |
20 | {authors.map((author) => (
21 |
22 |
27 |
28 | {AUTHORS[author].name}
29 |
30 |
31 | ))}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/components/home/blob.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useEffect, useState } from "react";
4 |
5 | export function Blob() {
6 | const [isSafari, setIsSafari] = useState(false);
7 |
8 | useEffect(() => {
9 | setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent));
10 | }, []);
11 |
12 | return (
13 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/apps/web/components/home/card.tsx:
--------------------------------------------------------------------------------
1 | export function Card({
2 | icon,
3 | title,
4 | description,
5 | }: {
6 | icon: React.ForwardRefExoticComponent<
7 | Omit, "ref"> & {
8 | title?: string | undefined;
9 | titleId?: string | undefined;
10 | } & React.RefAttributes
11 | >;
12 | title: string;
13 | description: string;
14 | }) {
15 | const Icon = icon;
16 |
17 | return (
18 |
19 |
20 |
{title}
21 |
{description}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/web/components/home/feature-card.tsx:
--------------------------------------------------------------------------------
1 | import { Pill } from "@floe/ui";
2 |
3 | const colors = {
4 | amber: "from-amber-500 bg-amber-400",
5 | rose: "from-rose-500 bg-rose-400",
6 | indigo: "from-indigo-500 bg-indigo-400",
7 | emerald: "from-emerald-500 bg-emerald-400",
8 | purple: "from-purple-500 bg-purple-400",
9 | };
10 |
11 | export function FeatureCard({
12 | pillText,
13 | title,
14 | description,
15 | color,
16 | children,
17 | }: {
18 | pillText: string;
19 | title: string;
20 | description: string;
21 | color: keyof typeof colors;
22 | children?: React.ReactNode;
23 | }) {
24 | return (
25 |
28 |
29 |
{title}
30 |
31 | {description.split("\\n").map((line, i) => (
32 |
33 | {line}
34 |
35 |
36 | ))}
37 |
38 | {children}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/apps/web/components/home/nav.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Image from "next/image";
5 | import logo from "public/logo-title.svg";
6 | import Link from "next/link";
7 | import { Button } from "@floe/ui";
8 |
9 | export function Nav() {
10 | return (
11 |
12 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/components/home/title.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState, useCallback } from "react";
4 |
5 | const TITLE = "Write Docs\n That Scale.\n Automatically.";
6 |
7 | export function Title() {
8 | const [text, setText] = useState("");
9 |
10 | const typeWriter = useCallback((i: number) => {
11 | const speed = 25 + Math.floor(Math.random() * 100);
12 |
13 | if (i < TITLE.length) {
14 | setText((prev) => prev + TITLE.charAt(i));
15 | setTimeout(() => {
16 | typeWriter(i + 1);
17 | }, speed);
18 | }
19 | }, []);
20 |
21 | useEffect(() => {
22 | if (TITLE !== text) {
23 | setTimeout(() => {
24 | typeWriter(0);
25 | }, 500);
26 | }
27 | // eslint-disable-next-line react-hooks/exhaustive-deps -- Including text here will cause an infinite loop
28 | }, [typeWriter]);
29 |
30 | const isDone = text.length === TITLE.length;
31 |
32 | return (
33 |
38 |
39 | {TITLE}
40 |
41 |
46 | {text}
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/apps/web/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | input {
6 | border: none;
7 | }
--------------------------------------------------------------------------------
/apps/web/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | const { withNextVideo } = require("next-video/process");
2 |
3 | const withNextra = require("nextra")({
4 | theme: "nextra-theme-docs",
5 | themeConfig: "./theme.config.jsx",
6 | });
7 |
8 | const withNextraConfig = withNextra({
9 | reactStrictMode: false,
10 | transpilePackages: ["@floe/ui"],
11 | });
12 |
13 | module.exports = withNextVideo(withNextraConfig, {
14 | provider: "vercel-blob",
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/web",
3 | "version": "0.1.0-beta.3",
4 | "description": "",
5 | "private": true,
6 | "main": "index.js",
7 | "scripts": {
8 | "dev": "next -p 3000 & npx next-video sync -w",
9 | "build": "next build",
10 | "start": "next start"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "@floe/ui": "workspace:*",
17 | "@headlessui/react": "^1.7.16",
18 | "@heroicons/react": "^2.0.18",
19 | "@vercel/analytics": "^1.1.1",
20 | "@vercel/blob": "^0.19.0",
21 | "classnames": "^2.3.2",
22 | "embla-carousel-react": "8.0.0-rc14",
23 | "next": "^14.0.2",
24 | "next-video": "^0.11.2",
25 | "nextra": "^2.13.2",
26 | "nextra-theme-docs": "^2.13.2",
27 | "react": "^18.2.0",
28 | "react-dom": "^18.2.0",
29 | "zod": "^3.22.4"
30 | },
31 | "devDependencies": {
32 | "@floe/tailwind": "workspace:*",
33 | "@next/eslint-plugin-next": "^13.4.19",
34 | "@types/node": "^17.0.12",
35 | "@types/react": "^18.0.22",
36 | "@types/react-dom": "^18.0.7",
37 | "autoprefixer": "^10.4.16",
38 | "eslint-config-custom": "workspace:*",
39 | "postcss": "^8.4.31",
40 | "tailwindcss": "^3.3.3",
41 | "tsconfig": "workspace:*",
42 | "typescript": "^5.3.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/apps/web/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import localFont from "next/font/local";
2 | import { Inter } from "next/font/google";
3 | import { Analytics } from "@vercel/analytics/react";
4 | import "../globals.css";
5 |
6 | const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
7 | const itcGaramondStd = localFont({
8 | src: "../public/itc-garamond-std.woff2",
9 | variable: "--font-itc-garamond-std",
10 | });
11 |
12 | export default function Nextra({ Component, pageProps }) {
13 | return (
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/pages/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": {
3 | "title": "Home",
4 | "type": "page",
5 | "display": "hidden",
6 | "theme": {
7 | "layout": "raw"
8 | }
9 | },
10 | "pricing": {
11 | "title": "Pricing",
12 | "type": "page",
13 | "theme": {
14 | "layout": "raw"
15 | }
16 | },
17 | "docs": {
18 | "title": "Docs",
19 | "type": "page"
20 | },
21 | "blog": {
22 | "title": "Blog",
23 | "type": "page",
24 | "display": "hidden",
25 | "theme": {
26 | "layout": "raw"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/apps/web/pages/blog.mdx:
--------------------------------------------------------------------------------
1 | import { BlogList } from "~/components/blog/list";
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/apps/web/pages/blog/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "*": {
3 | "theme": {
4 | "toc": false,
5 | "sidebar": false,
6 | "pagination": true,
7 | "typesetting": "article",
8 | "layout": "default",
9 | "breadcrumb": false
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/pages/blog/first-post.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Lorem Ipsum"
3 | date: "Mon Jan 08 2024 17:02:45 GMT-0500"
4 | subheading: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
5 | authors: "nic"
6 | image: "/avatar-nic.jpeg"
7 | ---
8 |
9 | import { BlogPost } from "~/components/blog/post"
10 |
11 | export default ({ children }) => {children};
12 |
13 | Introduce a typo into this post.
14 |
15 | Make a new mistake.
--------------------------------------------------------------------------------
/apps/web/pages/blog/second-post.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Lorem Ipsum"
3 | date: "Mon Jan 08 2024 17:02:45 GMT-0500"
4 | subheading: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
5 | authors: "nic"
6 | image: "/avatar-nic.jpeg"
7 | ---
8 |
9 | import { BlogPost } from "~/components/blog/post"
10 |
11 | export default ({ children }) => {children};
12 |
13 | Oops I mde a splling mistake.
--------------------------------------------------------------------------------
/apps/web/pages/docs/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": "Overview",
3 | "-- Getting started": {
4 | "type": "separator",
5 | "title": "Getting started"
6 | },
7 | "quick-start": "Quick start",
8 | "usage": "Basic usage",
9 | "configuration": "Configuration",
10 | "ci": "CI",
11 | "-- API reference": {
12 | "type": "separator",
13 | "title": "API reference"
14 | },
15 | "cli": "CLI",
16 | "-- Developer guide": {
17 | "type": "separator",
18 | "title": "Developer guide"
19 | },
20 | "prerequisites": "Prerequisites",
21 | "installation": "Installation"
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web/pages/docs/ci.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # CI
4 |
5 | Using the Floe Reviews GitHub Action is the easiest way to integrate Floe
6 | Reviews into your CI pipeline. The action will automatically review your pull
7 | request diffs and leave comments for issues and suggested fixes.
8 |
9 | ## Usage
10 |
11 | Copy the following workflow into the repository's `.github/workflows` directory.
12 |
13 | Be sure to set `FLOE_API_WORKSPACE` and
14 | `FLOE_API_SECRET` on the "Actions secrets and variables" page in your GitHub
15 | settings. `GITHUB_TOKEN` is available by default.
16 |
17 | ```yaml copy filename=".github/workflows/floe-review.yml"
18 | name: "Floe Review"
19 | on:
20 | pull_request:
21 |
22 | jobs:
23 | Review:
24 | env:
25 | FLOE_API_WORKSPACE: ${{ secrets.FLOE_API_WORKSPACE }}
26 | FLOE_API_SECRET: ${{ secrets.FLOE_API_SECRET }}
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 | runs-on: ubuntu-latest
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v4
32 |
33 | # Checkout the Floe repository
34 | - name: Checkout Floe
35 | uses: actions/checkout@v4
36 | with:
37 | repository: Floe-dev/floe
38 | ref: "@floe/review-action@0.1.0-beta.6"
39 | path: ./.github/actions/floe-actions
40 |
41 | # Run the review action
42 | - name: Review
43 | uses: ./.github/actions/floe-actions/actions/review-action
44 | ```
45 |
46 | ## Versioning
47 |
48 | The Floe Review GitHub Action is versioned using the `ref` attribute and passing
49 | a git tag. You can find the latest `@floe/review-action`
50 | [here](https://github.com/Floe-dev/floe/tags).
51 |
52 | You can keep Floe up to date with the latest versioning by removing the `ref`,
53 | but this is not recommended while Floe is in beta.
--------------------------------------------------------------------------------
/apps/web/pages/docs/index.mdx:
--------------------------------------------------------------------------------
1 | # Overview
2 | **Floe** is an AI technical writing assistant. Floe can make it easier to write and maintain documentation at scale by:
3 | - **Validating** documentation against rules
4 | - **Suggesting** improvements to documentation
5 | - **Writing** documentation from code *(coming soon)*
6 | - **Updating** documentation when code changes *(coming soon)*
7 | - **Translating** documentation into other languages *(coming soon)*
8 |
9 | ### Early access
10 | Floe is currently in beta. You may encounter bugs 🐛! If you have any feedback or suggestions, please let me know by emailing [nic@floe.dev](mailto:nic@floe.dev).
11 |
--------------------------------------------------------------------------------
/apps/web/pages/docs/prerequisites.mdx:
--------------------------------------------------------------------------------
1 | # Prerequisites
2 | The following guide will walk you through setting up your own instance of Floe in your local development.
3 |
4 | Before you begin, be sure to create accounts with the following services and
5 | install the following packages. All 3rd party services should have a free tier
6 | available.
7 |
8 | macOS, Windows and Linux are supported.
9 |
10 | ### Accounts
11 | - [GitHub](https://github.com/)
12 | - [Vercel](https://vercel.com/)
13 | - [OpenAI](https://openai.com/)
14 | - [SendGrid](https://sendgrid.com/) (Optional)
15 | - [LangFuse](https://langfuse.com/) (Optional)
16 | - [PlanetScale](https://planetscale.com/) (Optional)
17 |
18 | ### Packages
19 | - [Node.js 18.19](https://nodejs.org/en) or later
20 | - [pnpm 8.x](https://pnpm.io)
21 | - [PlanetScale CLI](https://planetscale.com/features/cli) (If using PlanetScale)
22 |
--------------------------------------------------------------------------------
/apps/web/pages/docs/usage.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 | import { Steps } from 'nextra/components'
3 |
4 | # Basic usage
5 | Let's use an example. Say we want to enforce specific terminology in our
6 | documentation.
7 |
8 | {/* These commands assume you have configured `.floe/config.json`. */}
9 |
10 | ### Add a rule
11 | ```markdown copy filename=".floe/rules/terminology.md"
12 | Make sure to always use the following terminology instead of these alternatives:
13 |
14 | - `main` instead of `master`
15 | - "Floe" instead of "Floe.dev"
16 | # ...
17 | ```
18 |
19 | ### Add the rule to a ruleset
20 | ```json copy filename=".floe/config.json"
21 | {
22 | "$schema": "https://unpkg.com/@floe/config@0.1.0-alpha.5/schema.json",
23 | "reviews": {
24 | "maxFileEvaluations": 5,
25 | "maxDiffEvaluations": 20
26 | },
27 | "rulesets": {
28 | "docs": {
29 | "include": ["**/*.mdx"],
30 | "rules": {
31 | "terminology": "error"
32 | }
33 | }
34 | }
35 | }
36 | ```
37 | ### Profit!
38 | We can now start using the CLI to review our changes.
39 |
40 | #### Review unstaged changes
41 | ```bash copy
42 | # This is the same as running `floe review diff HEAD`
43 | floe review diff
44 | ```
45 |
46 | #### Review the last commit and fix it
47 | ```bash copy
48 | floe review diff HEAD~1 --fix
49 | ```
50 |
51 | #### Review an existing file
52 | ```bash copy
53 | floe review files some/path/to/file.md
54 | ```
55 |
--------------------------------------------------------------------------------
/apps/web/pages/index.mdx:
--------------------------------------------------------------------------------
1 | import Home from "../components/home";
2 |
3 |
4 |
--------------------------------------------------------------------------------
/apps/web/pages/pricing.mdx:
--------------------------------------------------------------------------------
1 | import { Pricing } from "~/components/pricing";
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/apps/web/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/web/public/avatar-nic.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/web/public/avatar-nic.jpeg
--------------------------------------------------------------------------------
/apps/web/public/ci-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/web/public/ci-example.png
--------------------------------------------------------------------------------
/apps/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/web/public/favicon.ico
--------------------------------------------------------------------------------
/apps/web/public/itc-garamond-std.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/web/public/itc-garamond-std.woff2
--------------------------------------------------------------------------------
/apps/web/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/noise.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/pencil-art.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/apps/web/public/pencil-art.png
--------------------------------------------------------------------------------
/apps/web/server.webpack.lock:
--------------------------------------------------------------------------------
1 | {
2 | "https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/rule-create-high-fps-xr0vXRV66mOUts5LGbSuiPGLgI1hXb.mp4": { "integrity": "sha512-jzJ13DqwevQJ+FxUJQs/A3fNzJwsKu4p9t2dxrRmSYmg7CZb/qbawvpugPx/zLJDkzzz38Cb2uaGAm41ouixjg==", "contentType": "video/mp4" },
3 | "https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/rule-review-voO0xrHQCQNMLosKi0dYiOW1GtX3ii.mp4": { "integrity": "sha512-J3efAE00munIW+Sqiy84IVw0Q+4FGz9LEy96dWXHoMz8COD9cj8fGNxWiIjxe2x8arIuRsOnc7+0nFRasQvoWw==", "contentType": "video/mp4" },
4 | "version": 1
5 | }
6 |
--------------------------------------------------------------------------------
/apps/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // tailwind config is required for editor support
2 | import sharedConfig from "@floe/tailwind/tailwind.config.js";
3 |
4 | const config = {
5 | presets: [sharedConfig],
6 | content: [
7 | "../../packages/ui/**/*.{ts,tsx,md,mdx}",
8 | "./components/**/*.{ts,tsx,md,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | fontFamily: {
13 | inter: ["var(--font-inter)"],
14 | garamond: ["var(--font-itc-garamond-std)"],
15 | },
16 |
17 | animation: {
18 | blob: "blob-transform 5s ease-in-out infinite both alternate, blob-movement 10s ease-in-out infinite both alternate",
19 | "fade-in": "fade-in 0.2s ease",
20 | "fade-out": "fade-out 0.2s ease",
21 | },
22 |
23 | keyframes: {
24 | "blob-transform": {
25 | "0%": undefined,
26 | "20%": { "border-radius": "37% 63% 51% 49% / 37% 65% 35% 63%;" },
27 | "40%": { "border-radius": "36% 64% 64% 36% / 64% 48% 52% 36%;" },
28 | "60%": { "border-radius": "37% 63% 51% 49% / 30% 30% 70% 70%;" },
29 | "80%": { "border-radius": "40% 60% 42% 58% / 41% 51% 49% 59%;" },
30 | "100%": { "border-radius": "33% 67% 70% 30% / 30% 30% 70% 70%;" },
31 | },
32 |
33 | "blob-movement": {
34 | "0%": {
35 | transform: "scale(1);",
36 | opacity: 0.9,
37 | },
38 | "50%": {
39 | transform: "scale(1.2);",
40 | opacity: 0.7,
41 | },
42 | "100%": {
43 | transform: "scale(1);",
44 | opacity: 0.9,
45 | },
46 | },
47 | },
48 |
49 | backgroundImage: {
50 | noise: "url('/noise.svg')",
51 | },
52 | },
53 | },
54 | };
55 |
56 | export default config;
57 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [{ "name": "next" }],
5 | "moduleResolution": "bundler",
6 | "paths": {
7 | "~/*": [
8 | "./*"
9 | ]
10 | },
11 | },
12 | "include": ["video.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
13 | "exclude": ["node_modules"]
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/types/frontmatter.ts:
--------------------------------------------------------------------------------
1 | export interface Frontmatter {
2 | title: string;
3 | subheading: string;
4 | date: string;
5 | authors: string[];
6 | image?: string;
7 | }
8 |
--------------------------------------------------------------------------------
/apps/web/video.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/web/videos/custom-rules.mp4.json:
--------------------------------------------------------------------------------
1 | {"status":"ready","originalFilePath":"videos/custom-rules.mp4","provider":"vercel-blob","providerMetadata":{"vercel-blob":{"url":"https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/videos/custom-rules-nipXJgOMY7qJnPDUWS7AyMAoI1jJwF.mp4","contentType":"video/mp4"}},"createdAt":1705960545573,"updatedAt":1705960559699,"size":999067,"sources":[{"src":"https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/videos/custom-rules-nipXJgOMY7qJnPDUWS7AyMAoI1jJwF.mp4","type":"video/mp4"}]}
--------------------------------------------------------------------------------
/apps/web/videos/review-fix.mp4.json:
--------------------------------------------------------------------------------
1 | {"status":"ready","originalFilePath":"videos/review-fix.mp4","provider":"vercel-blob","providerMetadata":{"vercel-blob":{"url":"https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/videos/review-fix-WJjIFvXNqzoESju6xWSwBKT4SU7BEk.mp4","contentType":"video/mp4"}},"createdAt":1705960545572,"updatedAt":1705960559353,"size":510777,"sources":[{"src":"https://jdwfuoipoelmobuh.public.blob.vercel-storage.com/videos/review-fix-WJjIFvXNqzoESju6xWSwBKT4SU7BEk.mp4","type":"video/mp4"}]}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "build": "turbo run build",
5 | "dev": "turbo run dev --filter=@floe/app --filter=@floe/api --filter=@floe/web",
6 | "lint": "turbo run lint",
7 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
8 | "db:generate": "turbo run db:generate",
9 | "db:push": "turbo run db:push",
10 | "db:studio": "npx prisma studio"
11 | },
12 | "devDependencies": {
13 | "eslint": "^8.48.0",
14 | "prettier": "^3.0.3",
15 | "tsconfig": "workspace:*",
16 | "turbo": "latest"
17 | },
18 | "packageManager": "pnpm@8.12.0",
19 | "name": "floe-ai",
20 | "dependencies": {
21 | "@changesets/cli": "^2.27.1"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/cli/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/cli/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/cli",
3 | "bin": {
4 | "floe": "./dist/index.js"
5 | },
6 | "version": "0.1.0-beta.14",
7 | "description": "",
8 | "main": "./dist/index.js",
9 | "types": "./dist/index.d.ts",
10 | "scripts": {
11 | "link-cli": "npm uninstall -g && npm i -g",
12 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
13 | "build": "node build",
14 | "dev": "node build --watch",
15 | "prepublish": "pnpm build"
16 | },
17 | "files": [
18 | "dist"
19 | ],
20 | "author": "",
21 | "devDependencies": {
22 | "@types/diff": "^5.0.9",
23 | "@types/node": "^20.8.10",
24 | "esbuild": "^0.19.5",
25 | "eslint-config-custom": "workspace:*",
26 | "tsconfig": "workspace:*"
27 | },
28 | "dependencies": {
29 | "@floe/config": "workspace:*",
30 | "@floe/features": "workspace:*",
31 | "@floe/lib": "workspace:*",
32 | "@floe/requests": "workspace:*",
33 | "@inquirer/prompts": "^3.3.0",
34 | "axios": "^1.6.0",
35 | "chalk": "^5.3.0",
36 | "cli-spinners": "^2.9.1",
37 | "commander": "^11.1.0",
38 | "diff": "^5.1.0",
39 | "dotenv": "^16.3.1",
40 | "glob": "^10.3.10",
41 | "minimatch": "^9.0.3",
42 | "ora": "^7.0.1",
43 | "simple-git": "^3.20.0",
44 | "unique-names-generator": "^4.7.1"
45 | },
46 | "publishConfig": {
47 | "access": "public"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/ai-create/index.ts:
--------------------------------------------------------------------------------
1 | import type { Command } from "commander";
2 | import { fromDiff } from "./diff";
3 |
4 | export function aiCreate(program: Command) {
5 | const generateProgram = program
6 | .command("ai-create")
7 | .description("Create content");
8 |
9 | fromDiff(generateProgram);
10 | }
11 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/init.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import { resolve } from "node:path";
3 | import type { Command } from "commander";
4 | import { confirm } from "@inquirer/prompts";
5 | import type { Config } from "@floe/config";
6 | import { defaultConfig } from "@floe/config";
7 | import { checkIfValidRoot } from "@floe/lib/check-if-valid-root";
8 |
9 | const chalkImport = import("chalk").then((m) => m.default);
10 |
11 | export function init(program: Command) {
12 | program
13 | .command("init")
14 | .description("Initialize Floe")
15 | .action(async () => {
16 | /**
17 | * Exit if not a valid git root
18 | */
19 | checkIfValidRoot(true);
20 |
21 | const chalk = await chalkImport;
22 |
23 | if (fs.existsSync(".floe")) {
24 | const overwriteAnswer = await confirm({
25 | message:
26 | "A `.floe` directory was detected. The contents will be overwritten. Do you want to continue?",
27 | });
28 |
29 | if (!overwriteAnswer) {
30 | process.exit(0);
31 | }
32 | }
33 |
34 | /**
35 | * TODO: Let user choose which templates to copy
36 | */
37 | // fs.cpSync(`${__dirname}/default-files/templates`, ".floe/templates", {
38 | // recursive: true,
39 | // });
40 |
41 | fs.cpSync(`${__dirname}/default-files/rules`, ".floe/rules", {
42 | recursive: true,
43 | });
44 |
45 | /**
46 | * Can augment default config with rules, rulesets, etc. based on user input
47 | */
48 | const config: Config = {
49 | ...defaultConfig,
50 | rulesets: {
51 | docs: {
52 | include: ["**/*.md", "**/*.mdx"],
53 | rules: {
54 | "spelling-and-grammar": "warn",
55 | },
56 | },
57 | },
58 | };
59 |
60 | fs.writeFileSync(
61 | resolve(".floe/config.json"),
62 | JSON.stringify(config, null, 2)
63 | );
64 |
65 | console.log(chalk.green("✔ Floe initialized successfully!"));
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/review/index.ts:
--------------------------------------------------------------------------------
1 | import type { Command } from "commander";
2 | import { diff } from "./diff";
3 | import { files } from "./files";
4 |
5 | export function review(program: Command) {
6 | const validateProgram = program
7 | .command("review")
8 | .description("Review content");
9 |
10 | diff(validateProgram);
11 | files(validateProgram);
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/src/default-files/rules/spelling-and-grammar.md:
--------------------------------------------------------------------------------
1 | Make sure to use proper spelling and grammar.
2 |
--------------------------------------------------------------------------------
/packages/cli/src/default-files/templates/release-note.md:
--------------------------------------------------------------------------------
1 | # Release Notes - {{product_name}} {{release_number}}
2 |
3 | ## {{ai "Release Date - YYYY-MM-DD" }}
4 |
5 | {{ai "High-level summary on changes" }}
6 |
7 | ### New features
8 |
9 | - **{{ai "Feature title" }}**
10 |
11 | {{ai "Feature description" }}
12 |
13 | ### Improvements
14 |
15 | - **{{ai "Improvement title" }}**
16 |
17 | {{ai "Improvement description" }}
18 |
19 | ### Bug fixes
20 |
21 | - **{{ai "Bug fix title" }}**
22 |
23 | {{ai "Bug fix description" }}
24 |
25 | {{ai "#if known issues" }}
26 |
27 | ### Known issues
28 |
29 | - **{{ai "Known issue title" }}**
30 |
31 | {{ai "Known issue description" }}
32 |
33 | {{ai "/if"}}
34 |
35 | {{ai "#if deprecated features" }}
36 |
37 | ### Deprecated features
38 |
39 | - **{{ai "Deprecated feature title" }}**
40 |
41 | {{ai "Deprecated feature description" }}
42 |
43 | {{ai "/if" }}
44 |
--------------------------------------------------------------------------------
/packages/cli/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import "dotenv/config";
4 | import { Command } from "commander";
5 | import { version } from "../package.json";
6 | import { init } from "./commands/init";
7 | import { review } from "./commands/review";
8 |
9 | const program = new Command();
10 |
11 | /**
12 | * COMMANDS
13 | */
14 | init(program);
15 | review(program);
16 |
17 | // Get version from npm package
18 | program.version(version);
19 |
20 | /**
21 | * PARSE
22 | */
23 | program.parse();
24 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/git.ts:
--------------------------------------------------------------------------------
1 | import { execSync } from "node:child_process";
2 |
3 | export const gitGithubOrGitlabOrgAndRepo = () => {
4 | const githubOrgAndRepo = getOrgAndRepo("github");
5 | const gitlabOrgAndRepo = getOrgAndRepo("gitlab");
6 |
7 | return githubOrgAndRepo || gitlabOrgAndRepo;
8 | };
9 |
10 | export const getOrgAndRepo = (provider: "github" | "gitlab") => {
11 | const gitRepoPath = "."; // Path to Git repository
12 |
13 | try {
14 | const val = execSync(`git -C ${gitRepoPath} remote -v`).toString();
15 | const remoteInfo = val.trim().split("\n");
16 |
17 | // Check if any remote URL points to GitHub
18 | const remote = remoteInfo.find((r) => r.includes(`${provider}.com`));
19 |
20 | if (!remote) {
21 | return null;
22 | }
23 |
24 | const githubMatch = /github\.com[/:]([^/]+)\/([^/]+)\.git/.exec(remote);
25 | const gitlabMatch = /gitlab\.com[/:]([^/]+)\/([^/]+)\.git/.exec(remote);
26 | const match = provider === "github" ? githubMatch : gitlabMatch;
27 |
28 | if (match) {
29 | const owner = match[1];
30 | const repo = match[2];
31 |
32 | return {
33 | owner,
34 | repo,
35 | };
36 | }
37 |
38 | return null;
39 | } catch (e: any) {
40 | console.error(`Error: ${e.message}`);
41 | process.exit(1);
42 | }
43 | };
44 |
45 | export const getDefaultBranch = () => {
46 | try {
47 | const val = execSync(
48 | `git remote show origin | sed -n '/HEAD branch/s/.*: //p'`,
49 | {
50 | encoding: "utf-8",
51 | }
52 | ).trim();
53 |
54 | return val;
55 | } catch (e: any) {
56 | console.error(`Error: ${e.message}`);
57 | process.exit(1);
58 | }
59 | };
60 |
61 | export const getCurrentBranch = () =>
62 | execSync("git rev-parse --abbrev-ref HEAD", {
63 | encoding: "utf-8",
64 | }).trim();
65 |
66 | export const getDiff = (basehead: string) => {
67 | /**
68 | * TODO: May need special handling for CI env
69 | */
70 |
71 | return execSync(`git diff ${basehead}`, {
72 | encoding: "utf-8",
73 | }).trim();
74 | };
75 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/lines-update.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 |
3 | /**
4 | * Update lines in a file
5 | */
6 | export function updateLines(
7 | filePath: string,
8 | startLine: number,
9 | endLine: number,
10 | textToReplace: string,
11 | replaceTextWithFix: string
12 | ) {
13 | // Read the contents of the file
14 | const fileContents = fs.readFileSync(filePath, "utf-8").split("\n");
15 |
16 | const linesWithoutFix = fileContents
17 | .slice(startLine - 1, startLine + textToReplace.split("\n").length - 1)
18 | .join("\n");
19 |
20 | // Check if the content is indeed replaceable.
21 | if (!linesWithoutFix.includes(textToReplace)) {
22 | console.warn(
23 | "Could not find text to replace. A previous fix may have already been applied to this line."
24 | );
25 | return;
26 | }
27 |
28 | // Replace the first instance of the original content with the suggested fix
29 | const newContent = linesWithoutFix.replace(textToReplace, replaceTextWithFix);
30 |
31 | /**
32 | * Replace the specified lines with new content
33 | */
34 | fileContents.splice(startLine - 1, endLine - startLine + 1, newContent);
35 |
36 | // Write the updated contents back to the file
37 | fs.writeFileSync(filePath, fileContents.join("\n"), "utf-8");
38 | }
39 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/logging.ts:
--------------------------------------------------------------------------------
1 | const chalkImport = import("chalk").then((m) => m.default);
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is the type returned by Axios
4 | export async function logAxiosError(error: any) {
5 | const chalk = await chalkImport;
6 |
7 | if (error.response) {
8 | // The request was made and the server responded with a status code
9 | // that falls out of the range of 2xx
10 | console.log(
11 | chalk.red(`${error.response.status}: ${error.response.data.message}`)
12 | );
13 | } else if (error.request) {
14 | // The request was made but no response was received
15 | // `error.request` is an instance of XMLHttpRequest in the browser
16 | // and an instance of http.ClientRequest in node.js
17 | console.log("Request error: ", error.request);
18 | } else {
19 | // Something happened in setting up the request that triggered an Error
20 | console.log("An error occurred: ", error.message);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/truncate.ts:
--------------------------------------------------------------------------------
1 | export function truncate(string: string, n: number, truncateStart = false) {
2 | if (string.length <= n) {
3 | return string;
4 | }
5 |
6 | if (truncateStart) {
7 | return `...${string.slice(string.length - n, string.length)}`;
8 | }
9 |
10 | return `${string.slice(0, n - 1)}...`;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "compilerOptions": {
4 | "lib": ["esnext"],
5 | "module": "node16",
6 | "target": "es2022",
7 | "moduleResolution": "node16",
8 | "outDir": "./dist",
9 | "resolveJsonModule": true,
10 | },
11 | "include": ["src", "../lib/check-if-valid-root.ts", "../lib/get-floe-config.ts"],
12 | "exclude": ["dist", "build", "node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/config/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/config/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/config/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/config/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/config
2 |
3 | ## 0.1.0-beta.7
4 |
5 | ### Minor Changes
6 |
7 | - Bump to beta version.
8 |
9 | ## 0.1.0-alpha.5
10 |
11 | ### Minor Changes
12 |
13 | - Adds CLI 'floe review files' command.
14 |
15 | ## 0.1.0-alpha.4
16 |
17 | ### Minor Changes
18 |
19 | - Change config schema.
20 |
21 | ## 0.1.0-alpha.2
22 |
23 | ### Minor Changes
24 |
25 | - Cut a new release for cli and config
26 |
27 | ## 0.1.0-alpha.0
28 |
29 | ### Minor Changes
30 |
31 | - Create initial alpha version.
32 |
--------------------------------------------------------------------------------
/packages/config/generate.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import { schemaString } from "./schema.module";
3 |
4 | const outputPath = "./schema.json";
5 |
6 | fs.writeFile(outputPath, schemaString, (err) => {
7 | if (err) throw err;
8 | });
9 |
--------------------------------------------------------------------------------
/packages/config/index.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "./types";
2 | import { version } from "./package.json";
3 |
4 | export const defaultConfig: Config = {
5 | $schema: `https://unpkg.com/@floe/config@${version}/schema.json`,
6 | reviews: {
7 | maxFileEvaluations: 5,
8 | maxDiffEvaluations: 20,
9 | },
10 | rulesets: {},
11 | } as const;
12 |
13 | export { validate } from "./validate";
14 | export type { Config } from "./types";
15 |
--------------------------------------------------------------------------------
/packages/config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/config",
3 | "version": "0.1.0-beta.8",
4 | "description": "",
5 | "scripts": {
6 | "generate": "ts-node generate --esm",
7 | "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
8 | "build": "tsc -p tsconfig.json",
9 | "dev": "tsc -p tsconfig.json --watch",
10 | "prepublish": "pnpm build"
11 | },
12 | "main": "./dist/index.js",
13 | "types": "./dist/index.d.ts",
14 | "files": [
15 | "schema.json",
16 | "dist"
17 | ],
18 | "author": "",
19 | "license": "ISC",
20 | "devDependencies": {
21 | "@types/node": "^20.8.2",
22 | "eslint-config-custom": "workspace:*",
23 | "ts-node": "^10.9.1",
24 | "tsconfig": "workspace:*",
25 | "typescript": "^5.2.2"
26 | },
27 | "dependencies": {
28 | "ajv": "^8.12.0",
29 | "ts-json-schema-generator": "^1.3.0"
30 | },
31 | "publishConfig": {
32 | "access": "public"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/config/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "$ref": "#/definitions/Config",
4 | "definitions": {
5 | "Config": {
6 | "type": "object",
7 | "properties": {
8 | "$schema": {
9 | "type": "string"
10 | },
11 | "reviews": {
12 | "type": "object",
13 | "properties": {
14 | "maxFileEvaluations": {
15 | "type": "number"
16 | },
17 | "maxDiffEvaluations": {
18 | "type": "number"
19 | }
20 | },
21 | "additionalProperties": false
22 | },
23 | "rulesets": {
24 | "type": "object",
25 | "additionalProperties": {
26 | "type": "object",
27 | "properties": {
28 | "include": {
29 | "type": "array",
30 | "items": {
31 | "type": "string"
32 | }
33 | },
34 | "rules": {
35 | "type": "object",
36 | "additionalProperties": {
37 | "type": "string",
38 | "enum": [
39 | "error",
40 | "warn"
41 | ]
42 | }
43 | }
44 | },
45 | "required": [
46 | "include",
47 | "rules"
48 | ],
49 | "additionalProperties": false
50 | }
51 | }
52 | },
53 | "required": [
54 | "$schema",
55 | "rulesets"
56 | ],
57 | "additionalProperties": false
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/packages/config/schema.module.ts:
--------------------------------------------------------------------------------
1 | import path from "node:path";
2 | import { createGenerator } from "ts-json-schema-generator";
3 |
4 | // /** @type {import('ts-json-schema-generator/dist/src/Config').Config} */
5 | const config = {
6 | path: path.resolve(__dirname, "./types.json"),
7 | tsconfig: path.resolve(__dirname, "./tsconfig.json"),
8 | type: "Config", // Or if you want to generate schema for that one type only
9 | };
10 |
11 | export const schema: unknown = createGenerator(config).createSchema(
12 | config.type
13 | );
14 | export const schemaString = JSON.stringify(schema, null, 2);
15 |
--------------------------------------------------------------------------------
/packages/config/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": false,
4 | "declaration": true,
5 | "declarationMap": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "inlineSources": false,
9 | "isolatedModules": true,
10 | "moduleResolution": "node",
11 | "noUnusedLocals": false,
12 | "noUnusedParameters": false,
13 | "preserveWatchOutput": true,
14 | "skipLibCheck": true,
15 | "strict": true,
16 | "strictNullChecks": true,
17 | "resolveJsonModule": true,
18 | "outDir": "./dist",
19 | },
20 | "include": ["."],
21 | "exclude": ["dist", "build", "node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/config/types.ts:
--------------------------------------------------------------------------------
1 | export interface Config {
2 | $schema: string;
3 | reviews?: {
4 | // Prevent file reviews from surpassing this limit. This avoids accidentally making lots of requests.
5 | maxFileEvaluations?: number;
6 | // Prevent diff reviews from surpassing this limit. This avoids accidentally making lots of requests.
7 | maxDiffEvaluations?: number;
8 | };
9 | // Rulesets are used to group rules together.
10 | rulesets: Record<
11 | string,
12 | {
13 | include: readonly string[];
14 | rules: Record;
15 | }
16 | >;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/config/validate.ts:
--------------------------------------------------------------------------------
1 | import Ajv from "ajv";
2 | import schema from "./schema.json";
3 |
4 | export const validate = (data: unknown) => {
5 | const ajv = new Ajv();
6 | const validateFn = ajv.compile(schema);
7 | const valid = validateFn(data);
8 |
9 | if (!validateFn(data)) {
10 | return {
11 | valid: false,
12 | errors: validateFn.errors,
13 | };
14 | }
15 |
16 | return {
17 | valid,
18 | errors: [],
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/packages/db/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=mysql://root@127.0.0.1:3309/floe?connection_limit=100
--------------------------------------------------------------------------------
/packages/db/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/db/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/db
2 |
3 | ## 0.1.0-beta.3
4 |
5 | ### Minor Changes
6 |
7 | - Add support for token usage and pro / basic models.
8 |
9 | ## 0.1.0-beta.2
10 |
11 | ### Minor Changes
12 |
13 | - 5f84851: Add support for token usage and pro / basic models.
14 |
15 | ## 0.1.0-beta.1
16 |
17 | ### Minor Changes
18 |
19 | - Bump to beta version.
20 |
21 | ## 0.1.0-alpha.0
22 |
23 | ### Minor Changes
24 |
25 | - Create initial alpha version.
26 |
--------------------------------------------------------------------------------
/packages/db/index.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | declare global {
4 | // allow global `var` declarations
5 | // eslint-disable-next-line no-var -- allow
6 | var db: PrismaClient | undefined;
7 | }
8 |
9 | const db =
10 | global.db ||
11 | new PrismaClient({
12 | log: ["query"],
13 | });
14 |
15 | if (process.env.NODE_ENV !== "production") global.db = db;
16 |
17 | export { db };
18 | export type { PrismaClient };
19 | export * from "@prisma/client";
20 |
--------------------------------------------------------------------------------
/packages/db/models/index.ts:
--------------------------------------------------------------------------------
1 | export * as price from "./price";
2 | export * as tokenUsage from "./token-usage";
3 | export * as subscription from "./subscription";
4 |
--------------------------------------------------------------------------------
/packages/db/models/price/index.ts:
--------------------------------------------------------------------------------
1 | import { db } from "../..";
2 | import type { Prisma } from "../..";
3 |
4 | export async function findOne<
5 | T extends Parameters[0]["include"],
6 | U extends Prisma.PriceGetPayload<{
7 | include: T;
8 | }>,
9 | >(priceId: string, include?: T): Promise {
10 | const price = (await db.price.findUnique({
11 | where: {
12 | stripePriceId: priceId,
13 | },
14 | include,
15 | })) as U;
16 |
17 | return price;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/db/models/product/index.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Floe-dev/floe/452fdfee2f871514ed7c019592d70b52802f0859/packages/db/models/product/index.ts
--------------------------------------------------------------------------------
/packages/db/models/subscription/constants.ts:
--------------------------------------------------------------------------------
1 | export const FREE_BASE_TOKEN_LIMIT = 250000;
2 | export const FREE_PRO_TOKEN_LIMIT = 25000;
3 |
--------------------------------------------------------------------------------
/packages/db/models/subscription/index.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { db } from "../..";
3 | import { FREE_BASE_TOKEN_LIMIT, FREE_PRO_TOKEN_LIMIT } from "./constants";
4 |
5 | const metadataSchema = z.object({
6 | base_tokens: z.coerce.number(),
7 | pro_tokens: z.coerce.number(),
8 | });
9 |
10 | export async function getTokenLimits(workspaceId: string) {
11 | const subscription = await db.subscription.findUnique({
12 | where: {
13 | workspaceId,
14 | },
15 | include: {
16 | price: {
17 | include: {
18 | product: true,
19 | },
20 | },
21 | },
22 | });
23 |
24 | // On free tier
25 | if (!subscription) {
26 | return {
27 | baseTokenLimit: FREE_BASE_TOKEN_LIMIT,
28 | proTokenLimit: FREE_PRO_TOKEN_LIMIT,
29 | };
30 | }
31 |
32 | let metadata: z.infer;
33 |
34 | try {
35 | metadata = metadataSchema.parse(subscription.price.product.metadata);
36 | } catch (e) {
37 | throw new Error(
38 | `Could not parse subscription metadata: ${(e as ErrorEvent).message}`
39 | );
40 | }
41 |
42 | return {
43 | baseTokenLimit: metadata.base_tokens,
44 | proTokenLimit: metadata.pro_tokens,
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/packages/db/models/token-usage/get-month-year.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Returns a Date object representing the first day of the current month and year.
3 |
4 | For example, if today is November 21, 2021, this function will return a Date object
5 | with the value of November 1, 2021 at 00:00:00.000 UTC.
6 | */
7 | export function getMonthYearTimestamp(): Date {
8 | const now = new Date();
9 | return new Date(Date.UTC(now.getFullYear(), now.getMonth(), 1));
10 | }
11 |
--------------------------------------------------------------------------------
/packages/db/models/token-usage/index.ts:
--------------------------------------------------------------------------------
1 | import { db } from "../..";
2 | import type { TokenUsage } from "../..";
3 | import { getMonthYearTimestamp } from "./get-month-year";
4 |
5 | export type FindOneResult = TokenUsage | null;
6 |
7 | export async function findOne(workspaceId: string): Promise {
8 | const monthYear = getMonthYearTimestamp();
9 |
10 | const tokenUsage = await db.tokenUsage.findUnique({
11 | where: {
12 | workspaceId_monthYear: {
13 | monthYear,
14 | workspaceId,
15 | },
16 | },
17 | });
18 |
19 | return tokenUsage;
20 | }
21 |
--------------------------------------------------------------------------------
/packages/db/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/db",
3 | "version": "0.1.0-beta.3",
4 | "description": "",
5 | "private": true,
6 | "sideEffects": false,
7 | "main": "src/index.ts",
8 | "types": "src/index.ts",
9 | "scripts": {
10 | "db:connect": "pscale connect floe-cloud staging --port 3309",
11 | "db:generate": "prisma generate",
12 | "db:push": "prisma db push",
13 | "db:studio": "prisma studio",
14 | "postinstall": "prisma generate",
15 | "seed": "ts-node prisma/seed.ts"
16 | },
17 | "prisma": {
18 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
19 | },
20 | "author": "",
21 | "devDependencies": {
22 | "@types/node": "^20.5.9",
23 | "eslint-config-custom": "workspace:*",
24 | "prisma": "^5.3.1",
25 | "ts-node": "^10.9.1",
26 | "tsconfig": "workspace:*",
27 | "typescript": "^5.2.2"
28 | },
29 | "dependencies": {
30 | "@prisma/client": "^5.3.1",
31 | "zod": "^3.22.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/db/prisma/seed.ts:
--------------------------------------------------------------------------------
1 | import { randomBytes } from "node:crypto";
2 | import { PrismaClient } from "@prisma/client";
3 |
4 | const prisma = new PrismaClient();
5 |
6 | async function main() {
7 | const workspace = await prisma.workspace.upsert({
8 | where: { slug: "test-workspace" },
9 | update: {},
10 | create: {
11 | name: "Test Workspace",
12 | slug: "test-workspace",
13 | },
14 | });
15 |
16 | console.log("💼 Workspace created: ", workspace);
17 |
18 | const user = await prisma.user.upsert({
19 | where: { email: "test@floe.dev" },
20 | update: {},
21 | create: {
22 | email: "test@floe.dev",
23 | name: "Testy McTestface",
24 | },
25 | });
26 |
27 | console.log("👤 User created: ", user);
28 |
29 | const workspaceMembership = await prisma.workspaceMembership.upsert({
30 | where: {
31 | workspaceId_userId: {
32 | workspaceId: workspace.id,
33 | userId: user.id,
34 | },
35 | },
36 | update: {},
37 | create: {
38 | workspaceId: workspace.id,
39 | userId: user.id,
40 | role: "OWNER",
41 | },
42 | });
43 |
44 | console.log("👥 WorkspaceMembership created: ", workspaceMembership);
45 |
46 | const fakeSession = randomBytes(16).toString("hex");
47 |
48 | // Date 1 month from now
49 | const currentDate = new Date();
50 | const futureDate = new Date();
51 | futureDate.setMonth(currentDate.getMonth() + 1);
52 |
53 | const session = await prisma.session.upsert({
54 | where: { sessionToken: fakeSession },
55 | update: {
56 | expires: futureDate,
57 | },
58 | create: {
59 | sessionToken: fakeSession,
60 | userId: user.id,
61 | expires: futureDate,
62 | },
63 | });
64 |
65 | console.log("🔐 Session created: ", session);
66 | console.log(
67 | "ℹ️ To log in, start the dev server and go to localhost:3001. Then run the following command in your console:\n"
68 | );
69 | console.log(`document.cookie="next-auth.session-token=${fakeSession}"`);
70 | }
71 |
72 | main()
73 | .then(async () => {
74 | await prisma.$disconnect();
75 | })
76 | .catch(async (e) => {
77 | console.error(e);
78 | await prisma.$disconnect();
79 | process.exit(1);
80 | });
81 |
--------------------------------------------------------------------------------
/packages/db/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # eslint-config-custom
2 |
3 | ## 0.1.0-beta.1
4 |
5 | ### Minor Changes
6 |
7 | - Bump to beta version.
8 |
9 | ## 0.1.0-alpha.0
10 |
11 | ### Minor Changes
12 |
13 | - Create initial alpha version.
14 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * typescript packages.
8 | *
9 | * This config extends the Vercel Engineering Style Guide.
10 | * For more information, see https://github.com/vercel/style-guide
11 | *
12 | */
13 |
14 | module.exports = {
15 | extends: [
16 | "@vercel/style-guide/eslint/node",
17 | "@vercel/style-guide/eslint/typescript",
18 | ].map(require.resolve),
19 | parserOptions: {
20 | project,
21 | },
22 | globals: {
23 | React: true,
24 | JSX: true,
25 | },
26 | settings: {
27 | "import/resolver": {
28 | typescript: {
29 | project,
30 | },
31 | },
32 | },
33 | ignorePatterns: ["node_modules/", "dist/"],
34 |
35 | rules: {
36 | // add specific rules configurations here
37 | "react/button-has-type": "off",
38 | "@typescript-eslint/no-misused-promises": "off",
39 | // Doesn't work with absolute imports
40 | "import/no-extraneous-dependencies": "off",
41 | "no-console": "warn",
42 | "@typescript-eslint/no-unsafe-member-access": "off",
43 | "@typescript-eslint/no-unsafe-return": "off",
44 | "@typescript-eslint/no-unsafe-assignment": "off",
45 | "import/no-named-as-default": "off",
46 | "@typescript-eslint/explicit-function-return-type": "off",
47 | "@typescript-eslint/no-unsafe-call": "off",
48 | "prefer-named-capture-group": "off",
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * Next.js apps.
8 | *
9 | * This config extends the Vercel Engineering Style Guide.
10 | * For more information, see https://github.com/vercel/style-guide
11 | *
12 | */
13 |
14 | module.exports = {
15 | extends: [
16 | "@vercel/style-guide/eslint/node",
17 | "@vercel/style-guide/eslint/browser",
18 | "@vercel/style-guide/eslint/typescript",
19 | "@vercel/style-guide/eslint/react",
20 | "@vercel/style-guide/eslint/next",
21 | "eslint-config-turbo",
22 | ].map(require.resolve),
23 | parserOptions: {
24 | project,
25 | },
26 | globals: {
27 | React: true,
28 | JSX: true,
29 | },
30 | settings: {
31 | "import/resolver": {
32 | typescript: {
33 | project,
34 | },
35 | },
36 | },
37 | ignorePatterns: ["node_modules/", "dist/"],
38 | // add rules configurations here
39 | rules: {
40 | "no-nested-ternary": "off",
41 | "import/no-default-export": "off",
42 | "@typescript-eslint/no-misused-promises": "off",
43 | // Doesn't work with absolute imports
44 | "import/no-extraneous-dependencies": "off",
45 | "no-console": "warn",
46 | "@typescript-eslint/no-unsafe-member-access": "off",
47 | "@typescript-eslint/no-unsafe-return": "off",
48 | "@typescript-eslint/no-unsafe-assignment": "off",
49 | "import/no-named-as-default": "off",
50 | "@typescript-eslint/explicit-function-return-type": "off",
51 | "@typescript-eslint/no-unsafe-call": "off",
52 | "prefer-named-capture-group": "off",
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-config-custom",
3 | "version": "0.1.0-beta.1",
4 | "private": true,
5 | "devDependencies": {
6 | "@vercel/style-guide": "^5.0.0",
7 | "eslint-config-turbo": "^1.10.12",
8 | "typescript": "^5.3.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/eslint-config-custom/react-internal.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require("node:path");
2 |
3 | const project = resolve(process.cwd(), "tsconfig.json");
4 |
5 | /*
6 | * This is a custom ESLint configuration for use with
7 | * internal (bundled by their consumer) libraries
8 | * that utilize React.
9 | *
10 | * This config extends the Vercel Engineering Style Guide.
11 | * For more information, see https://github.com/vercel/style-guide
12 | *
13 | */
14 |
15 | module.exports = {
16 | extends: [
17 | "@vercel/style-guide/eslint/browser",
18 | "@vercel/style-guide/eslint/typescript",
19 | "@vercel/style-guide/eslint/react",
20 | ].map(require.resolve),
21 | parserOptions: {
22 | project,
23 | },
24 | globals: {
25 | JSX: true,
26 | },
27 | settings: {
28 | "import/resolver": {
29 | typescript: {
30 | project,
31 | },
32 | },
33 | },
34 | ignorePatterns: ["node_modules/", "dist/", ".eslintrc.js"],
35 |
36 | rules: {
37 | // add specific rules configurations here
38 | "react/button-has-type": "off",
39 | "@typescript-eslint/no-misused-promises": "off",
40 | // Doesn't work with absolute imports
41 | "import/no-extraneous-dependencies": "off",
42 | "no-console": "warn",
43 | "@typescript-eslint/no-unsafe-member-access": "off",
44 | "@typescript-eslint/no-unsafe-return": "off",
45 | "@typescript-eslint/no-unsafe-assignment": "off",
46 | "import/no-named-as-default": "off",
47 | "@typescript-eslint/explicit-function-return-type": "off",
48 | "@typescript-eslint/no-unsafe-call": "off",
49 | "prefer-named-capture-group": "off",
50 | },
51 | };
52 |
--------------------------------------------------------------------------------
/packages/features/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/features/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/features
2 |
3 | ## 0.1.0-beta.6
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/lib@0.1.0-beta.5
13 | - @floe/requests@0.1.0-beta.7
14 |
15 | ## 0.1.0-beta.5
16 |
17 | ### Minor Changes
18 |
19 | - 5f84851: Add support for token usage and pro / basic models.
20 |
21 | ### Patch Changes
22 |
23 | - Updated dependencies [5f84851]
24 | - @floe/requests@0.1.0-beta.6
25 | - @floe/lib@0.1.0-beta.4
26 |
27 | ## 0.1.0-beta.4
28 |
29 | ### Patch Changes
30 |
31 | - Updated dependencies
32 | - @floe/requests@0.1.0-beta.5
33 |
34 | ## 0.1.0-beta.3
35 |
36 | ### Patch Changes
37 |
38 | - Updated dependencies [2cae03a]
39 | - @floe/requests@0.1.0-beta.4
40 |
41 | ## 0.1.0-beta.2
42 |
43 | ### Minor Changes
44 |
45 | - Stability fixes to reviews.
46 |
47 | ### Patch Changes
48 |
49 | - Updated dependencies
50 | - @floe/requests@0.1.0-beta.3
51 |
--------------------------------------------------------------------------------
/packages/features/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/features",
3 | "sideEffects": false,
4 | "version": "0.1.0-beta.6",
5 | "dependencies": {
6 | "@floe/lib": "workspace:*",
7 | "@floe/requests": "workspace:*",
8 | "minimatch": "^9.0.3"
9 | },
10 | "devDependencies": {
11 | "@types/node": "^20.8.10",
12 | "eslint-config-custom": "workspace:*",
13 | "tsconfig": "workspace:*"
14 | },
15 | "publishConfig": {
16 | "access": "public"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/features/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/lib/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/lib/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/lib
2 |
3 | ## 0.1.0-beta.5
4 |
5 | ### Minor Changes
6 |
7 | - Add support for token usage and pro / basic models.
8 |
9 | ## 0.1.0-beta.4
10 |
11 | ### Minor Changes
12 |
13 | - 5f84851: Add support for token usage and pro / basic models.
14 |
15 | ## 0.1.0-beta.2
16 |
17 | ### Minor Changes
18 |
19 | - Bump to beta version.
20 |
21 | ## 0.1.0-alpha.1
22 |
23 | ### Minor Changes
24 |
25 | - Adds CLI 'floe review files' command.
26 |
27 | ## 0.1.0-alpha.0
28 |
29 | ### Minor Changes
30 |
31 | - c8fa9fd: Improve error handling.
32 |
--------------------------------------------------------------------------------
/packages/lib/capitalize.ts:
--------------------------------------------------------------------------------
1 | export function capitalize(str: string): string {
2 | return str.charAt(0).toUpperCase() + str.slice(1);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/lib/check-if-valid-root.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 |
3 | export const checkIfValidRoot = (ignoreFloeDir = false) => {
4 | const gitDir = fs.existsSync(".git");
5 | const floeDir = fs.existsSync(".floe");
6 |
7 | if (!gitDir) {
8 | console.log("Floe commands must be run from the root of a git repository.");
9 | process.exit(0);
10 | }
11 |
12 | if (!floeDir && !ignoreFloeDir) {
13 | console.log(
14 | "No .floe directory found. Are you sure you have initialized Floe?"
15 | );
16 | process.exit(0);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/packages/lib/class-names.ts:
--------------------------------------------------------------------------------
1 | export function classNames(...classes: string[]) {
2 | return classes.filter(Boolean).join(" ");
3 | }
4 |
--------------------------------------------------------------------------------
/packages/lib/diff-parser.ts:
--------------------------------------------------------------------------------
1 | import gitDiffParser from "gitdiff-parser";
2 |
3 | /**
4 | * Returns a list of Files containing Hunks
5 | */
6 | export function parseDiffToFileHunks(diffText: string) {
7 | const files = gitDiffParser.parse(diffText);
8 |
9 | // Floe will only comment on NEW changes.
10 | // We only want to collect new and normal lines
11 | return files
12 | .filter((f) => f.type !== "delete")
13 | .map((f) => {
14 | return {
15 | path: f.newPath,
16 | hunks: f.hunks.map((h) => {
17 | return {
18 | startLine: h.newStart,
19 | content: h.changes.reduce((acc, c) => {
20 | if (c.type === "insert") {
21 | return `${acc}${c.content}\n`;
22 | }
23 |
24 | if (c.type === "normal") {
25 | return `${acc}${c.content}\n`;
26 | }
27 |
28 | return acc;
29 | }, ""),
30 | };
31 | }),
32 | };
33 | });
34 | }
35 |
36 | export type File = ReturnType[number];
37 |
--------------------------------------------------------------------------------
/packages/lib/encryption.ts:
--------------------------------------------------------------------------------
1 | import crypto from "node:crypto";
2 |
3 | const envryptionMethod = "aes-256-cbc";
4 |
5 | if (!process.env.FLOE_SECRET_KEY || !process.env.FLOE_SECRET_IV) {
6 | throw new Error("floeSecretKey and floeSecretIV are required");
7 | }
8 |
9 | // Generate secret hash with crypto to use for encryption
10 | const key = crypto
11 | .createHash("sha512")
12 | .update(process.env.FLOE_SECRET_KEY)
13 | .digest("hex")
14 | .substring(0, 32);
15 | const encryptionIV = crypto
16 | .createHash("sha512")
17 | .update(process.env.FLOE_SECRET_IV)
18 | .digest("hex")
19 | .substring(0, 16);
20 |
21 | // Encrypt data
22 | export function encryptData(data: string) {
23 | const cipher = crypto.createCipheriv(envryptionMethod, key, encryptionIV);
24 | return Buffer.from(
25 | cipher.update(data, "utf8", "hex") + cipher.final("hex")
26 | ).toString("base64"); // Encrypts data and converts to hex and base64
27 | }
28 |
29 | // Decrypt data
30 | export function decryptData(encryptedData: string) {
31 | const buff = Buffer.from(encryptedData, "base64");
32 | const decipher = crypto.createDecipheriv(envryptionMethod, key, encryptionIV);
33 | return (
34 | decipher.update(buff.toString("utf8"), "hex", "utf8") +
35 | decipher.final("utf8")
36 | ); // Decrypts data and converts to utf8
37 | }
38 |
--------------------------------------------------------------------------------
/packages/lib/get-floe-config.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 | import type { Config } from "@floe/config";
4 |
5 | export function getFloeConfig() {
6 | try {
7 | const config = fs.readFileSync(
8 | path.join(process.cwd(), ".floe/config.json"),
9 | "utf-8"
10 | );
11 |
12 | return JSON.parse(config) as Config;
13 | } catch (e) {
14 | console.log(`Invalid config. Please run "floe init" to initialize Floe.`);
15 |
16 | process.exit(1);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/lib/get-month-year.ts:
--------------------------------------------------------------------------------
1 | /**
2 | Returns a Date object representing the first day of the current month and year.
3 |
4 | For example, if today is November 21, 2021, this function will return a Date object
5 | with the value of November 1, 2021 at 00:00:00.000 UTC.
6 | */
7 | export function getMonthYearTimestamp(): Date {
8 | const now = new Date();
9 | return new Date(Date.UTC(now.getFullYear(), now.getMonth(), 1));
10 | }
11 |
--------------------------------------------------------------------------------
/packages/lib/http-error.ts:
--------------------------------------------------------------------------------
1 | type StatusCode =
2 | | 400
3 | | 401
4 | | 402
5 | | 403
6 | | 404
7 | | 405
8 | | 429
9 | | 500
10 | | 501
11 | | 502
12 | | 503
13 | | 504;
14 |
15 | export class HttpError extends Error {
16 | public readonly statusCode: StatusCode;
17 | public readonly message: string;
18 | public readonly log: boolean;
19 |
20 | constructor(opts: {
21 | statusCode: StatusCode;
22 | message: string;
23 | log?: boolean;
24 | }) {
25 | super(opts.message);
26 |
27 | Object.setPrototypeOf(this, HttpError.prototype);
28 | this.name = HttpError.prototype.constructor.name;
29 |
30 | this.statusCode = opts.statusCode;
31 | this.message = opts.message;
32 | this.log = opts.log || false;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/lib/not-empty.ts:
--------------------------------------------------------------------------------
1 | export function notEmpty(
2 | value: TValue | null | undefined
3 | ): value is TValue {
4 | return value !== null && value !== undefined;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/lib",
3 | "sideEffects": false,
4 | "version": "0.1.0-beta.5",
5 | "dependencies": {
6 | "axios": "^1.6.0",
7 | "gitdiff-parser": "^0.3.1",
8 | "minimatch": "^9.0.3",
9 | "slugify": "^1.6.6"
10 | },
11 | "devDependencies": {
12 | "@floe/config": "workspace:*",
13 | "@types/node": "^20.8.10",
14 | "eslint-config-custom": "workspace:*",
15 | "tsconfig": "workspace:*"
16 | },
17 | "publishConfig": {
18 | "access": "public"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/lib/pluralize.ts:
--------------------------------------------------------------------------------
1 | export function pluralize(count: number, singular: string, plural: string) {
2 | return count === 1 ? singular : plural;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/lib/rules.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs";
2 | import path from "node:path";
3 | import { getFloeConfig } from "./get-floe-config";
4 |
5 | export const getRulesets = (ruleset?: string) => {
6 | const { rulesets } = getFloeConfig();
7 |
8 | const rulesetsWithRules = Object.entries(rulesets)
9 | .filter(([key]) => {
10 | // Only return ruleset if it matches the ruleset argument
11 | if (ruleset) {
12 | return key === ruleset;
13 | }
14 | return true;
15 | })
16 | .map(([key, value]) => {
17 | return {
18 | name: key,
19 | ...value,
20 | rules: Object.entries(value.rules).map(([ruleKey, ruleValue]) => {
21 | let rule;
22 |
23 | try {
24 | rule = fs.readFileSync(
25 | path.join(process.cwd(), `.floe/rules/${ruleKey}.md`),
26 | "utf-8"
27 | );
28 | } catch (e) {
29 | console.log(
30 | `Invalid config. Rule "${ruleKey}" does not exist in "rules" directory.`
31 | );
32 |
33 | process.exit(1);
34 | }
35 |
36 | return {
37 | code: ruleKey,
38 | level: ruleValue,
39 | description: rule,
40 | };
41 | }),
42 | };
43 | });
44 |
45 | return rulesetsWithRules;
46 | };
47 |
48 | export type Ruleset = ReturnType[number];
49 |
--------------------------------------------------------------------------------
/packages/lib/slugify.ts:
--------------------------------------------------------------------------------
1 | import s from "slugify";
2 |
3 | export const slugify = (string: string) =>
4 | s(string, {
5 | lower: true,
6 | strict: true,
7 | });
8 |
--------------------------------------------------------------------------------
/packages/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/requests/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/library"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/requests/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/requests
2 |
3 | ## 0.1.0-beta.7
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/lib@0.1.0-beta.5
13 |
14 | ## 0.1.0-beta.6
15 |
16 | ### Minor Changes
17 |
18 | - 5f84851: Add support for token usage and pro / basic models.
19 |
20 | ### Patch Changes
21 |
22 | - Updated dependencies [5f84851]
23 | - @floe/lib@0.1.0-beta.4
24 |
25 | ## 0.1.0-beta.5
26 |
27 | ### Minor Changes
28 |
29 | - Update some API paths. This introduces a breaking change to the CLI ‼️
30 |
31 | ## 0.1.0-beta.4
32 |
33 | ### Minor Changes
34 |
35 | - 2cae03a: Make @floe/requests public.
36 |
37 | ## 0.1.0-beta.3
38 |
39 | ### Minor Changes
40 |
41 | - Stability fixes to reviews.
42 |
43 | ## 0.1.0-beta.2
44 |
45 | ### Minor Changes
46 |
47 | - Bump to beta version.
48 |
49 | ### Patch Changes
50 |
51 | - Updated dependencies
52 | - @floe/lib@0.1.0-beta.2
53 |
54 | ## 0.1.0-alpha.1
55 |
56 | ### Patch Changes
57 |
58 | - Updated dependencies
59 | - @floe/lib@0.1.0-alpha.1
60 |
--------------------------------------------------------------------------------
/packages/requests/ai-create-diff/_get.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAI } from "openai";
2 |
3 | export type AiCreateDiffResponse =
4 | | OpenAI.Chat.Completions.ChatCompletion
5 | | undefined;
6 |
--------------------------------------------------------------------------------
/packages/requests/api.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | export function getBaseUrl() {
4 | if (process.env.FLOE_API_ENDPOINT)
5 | // reference for vercel.com
6 | return process.env.FLOE_API_ENDPOINT;
7 |
8 | // assume localhost
9 | return "https://api.floe.dev";
10 | }
11 |
12 | const floeApiSecret = process.env.FLOE_API_SECRET;
13 | const floeApiWorkspace = process.env.FLOE_API_WORKSPACE;
14 |
15 | export const api = axios.create({
16 | baseURL: getBaseUrl(),
17 | headers: {
18 | ...(floeApiSecret && { "x-api-key": floeApiSecret }),
19 | ...(floeApiWorkspace && { "x-api-workspace": floeApiWorkspace }),
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/packages/requests/git/issue-comments/[comment_id]/_patch.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import type { Endpoints } from "@octokit/types";
3 | import { api } from "../../../api";
4 |
5 | export const querySchema = z.object({
6 | body: z.string(),
7 | repo: z.string(),
8 | owner: z.string(),
9 | commentId: z.coerce.number(),
10 | });
11 |
12 | export type PatchGitIssueCommentsResponse =
13 | Endpoints["PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}"]["response"]["data"];
14 |
15 | export type PatchIssueCommentsInput = z.infer;
16 |
17 | export async function updateGitIssueComment({
18 | body,
19 | repo,
20 | owner,
21 | commentId,
22 | }: PatchIssueCommentsInput) {
23 | return api.patch(
24 | `/api/v1/git/issue-comments/${commentId}`,
25 | {
26 | body,
27 | repo,
28 | owner,
29 | }
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/requests/git/issue-comments/_get.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import type { Endpoints } from "@octokit/types";
3 | import { api } from "../../api";
4 |
5 | export const querySchema = z.object({
6 | owner: z.string(),
7 | repo: z.string(),
8 | issueNumber: z.coerce.number(),
9 | });
10 |
11 | export type GetGitIssueCommentsResponse =
12 | Endpoints["GET /repos/{owner}/{repo}/issues/comments"]["response"]["data"];
13 |
14 | export type GetIssueCommentsInput = z.infer;
15 |
16 | export async function fetchGitIssueComments({
17 | owner,
18 | repo,
19 | issueNumber,
20 | }: GetIssueCommentsInput) {
21 | return api.get("/api/v1/git/issue-comments", {
22 | params: {
23 | owner,
24 | repo,
25 | issueNumber,
26 | },
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/packages/requests/git/issue-comments/_post.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import type { Endpoints } from "@octokit/types";
3 | import { api } from "../../api";
4 |
5 | export const querySchema = z.object({
6 | repo: z.string(),
7 | body: z.string(),
8 | owner: z.string(),
9 | issueNumber: z.coerce.number(),
10 | });
11 |
12 | export type PostGitIssueCommentsResponse =
13 | Endpoints["POST /repos/{owner}/{repo}/issues/{issue_number}/comments"]["response"]["data"];
14 |
15 | export type PostIssueCommentsInput = z.infer;
16 |
17 | export async function createGitIssueComment({
18 | repo,
19 | owner,
20 | body,
21 | issueNumber,
22 | }: PostIssueCommentsInput) {
23 | return api.post("/api/v1/git/issue-comments", {
24 | repo,
25 | owner,
26 | body,
27 | issueNumber,
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/packages/requests/git/review-comments/_get.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import type { Endpoints } from "@octokit/types";
3 | import { api } from "../../api";
4 |
5 | export const querySchema = z.object({
6 | owner: z.string(),
7 | repo: z.string(),
8 | pullNumber: z.coerce.number(),
9 | });
10 |
11 | export type GetGitReviewCommentsResponse =
12 | Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}/comments"]["response"]["data"];
13 |
14 | export type GetReviewCommentsInput = z.infer;
15 |
16 | export async function fetchGitReviewComments({
17 | owner,
18 | repo,
19 | pullNumber,
20 | }: GetReviewCommentsInput) {
21 | return api.get("/api/v1/git/review-comments", {
22 | params: {
23 | owner,
24 | repo,
25 | pullNumber,
26 | },
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/packages/requests/git/review-comments/_post.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import type { Endpoints } from "@octokit/types";
3 | import { api } from "../../api";
4 |
5 | export const querySchema = z.object({
6 | path: z.string(),
7 | repo: z.string(),
8 | body: z.string(),
9 | owner: z.string(),
10 | commitId: z.string(),
11 | pullNumber: z.coerce.number(),
12 | line: z.coerce.number().optional(),
13 | startLine: z.coerce.number().optional(),
14 | side: z.enum(["LEFT", "RIGHT"]).optional(),
15 | startSide: z.enum(["LEFT", "RIGHT"]).optional(),
16 | });
17 |
18 | export type PostGitReviewCommentsResponse =
19 | Endpoints["POST /repos/{owner}/{repo}/pulls/{pull_number}/comments"]["response"]["data"];
20 |
21 | export type PostReviewCommentsInput = z.infer;
22 |
23 | export async function createGitReviewComment({
24 | path,
25 | repo,
26 | owner,
27 | body,
28 | commitId,
29 | pullNumber,
30 | line,
31 | startLine,
32 | side,
33 | startSide,
34 | }: PostReviewCommentsInput) {
35 | return api.post(
36 | "/api/v1/git/review-comments",
37 | {
38 | path,
39 | repo,
40 | owner,
41 | body,
42 | commitId,
43 | pullNumber,
44 | line,
45 | startLine,
46 | side,
47 | startSide,
48 | }
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/packages/requests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/requests",
3 | "version": "0.1.0-beta.7",
4 | "dependencies": {
5 | "@floe/lib": "workspace:*",
6 | "axios": "^1.6.0",
7 | "zod": "^3.22.4"
8 | },
9 | "devDependencies": {
10 | "@octokit/types": "^12.3.0",
11 | "@types/node": "^20.8.10",
12 | "openai": "^4.19.0",
13 | "eslint-config-custom": "workspace:*",
14 | "tsconfig": "workspace:*"
15 | },
16 | "publishConfig": {
17 | "access": "public"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/requests/review/_post.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import type OpenAI from "openai";
3 | import { api } from "../api";
4 |
5 | export const querySchema = z.object({
6 | path: z.string(),
7 | content: z.string(),
8 | startLine: z.coerce.number().default(1),
9 | rule: z.object({
10 | code: z.string(),
11 | level: z.union([z.literal("error"), z.literal("warn")]),
12 | description: z.string(),
13 | }),
14 | model: z.union([z.literal("pro"), z.literal("basic")]).default("pro"),
15 | });
16 |
17 | export type PostReviewResponse =
18 | | {
19 | violations: {
20 | // A description of the violation
21 | description: string | undefined;
22 | // This is `replaceTextWithFix`, but with the rest of the line(s) content include
23 | linesWithFix: string | undefined;
24 | // The original content that should be replaced
25 | linesWithoutFix: string;
26 | // The first line number of the violation
27 | startLine: number;
28 | // The last line number of the violation
29 | endLine: number;
30 | // The specific string that should be replaced (generated by LLM)
31 | textToReplace: string;
32 | // The suggested fix (generated by LLM)
33 | replaceTextWithFix: string;
34 | }[];
35 | rule: {
36 | level: "error" | "warn" | undefined;
37 | code: string;
38 | description: string;
39 | };
40 | path: string;
41 | cached: boolean;
42 | model: string;
43 | usage: OpenAI.Completions.CompletionUsage | undefined;
44 | }
45 | | undefined;
46 | export type PostReviewInput = z.input;
47 |
48 | export async function createReview({
49 | path,
50 | content,
51 | startLine,
52 | rule,
53 | model,
54 | }: PostReviewInput) {
55 | return api.post("/api/v1/review", {
56 | path,
57 | content,
58 | startLine,
59 | rule,
60 | model,
61 | });
62 | }
63 |
--------------------------------------------------------------------------------
/packages/requests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/tailwind/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/tailwind
2 |
3 | ## 0.1.0-beta.1
4 |
5 | ### Minor Changes
6 |
7 | - Bump to beta version.
8 |
9 | ## 0.1.0-alpha.0
10 |
11 | ### Minor Changes
12 |
13 | - Create initial alpha version.
14 |
--------------------------------------------------------------------------------
/packages/tailwind/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/tailwind",
3 | "version": "0.1.0-beta.1",
4 | "private": true,
5 | "main": "index.js",
6 | "devDependencies": {
7 | "@tailwindcss/forms": "^0.5.4",
8 | "@tailwindcss/typography": "^0.5.10",
9 | "tailwindcss": "^3.3.3"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/tailwind/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 |
--------------------------------------------------------------------------------
/packages/tsconfig/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # tsconfig
2 |
3 | ## 0.1.0-beta.1
4 |
5 | ### Minor Changes
6 |
7 | - Bump to beta version.
8 |
9 | ## 0.1.0-alpha.0
10 |
11 | ### Minor Changes
12 |
13 | - Create initial alpha version.
14 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "preserveWatchOutput": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "strictNullChecks": true
19 | },
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/packages/tsconfig/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "allowJs": true,
8 | "declaration": false,
9 | "declarationMap": false,
10 | "incremental": true,
11 | "jsx": "preserve",
12 | "lib": ["dom", "dom.iterable", "esnext"],
13 | "module": "esnext",
14 | "noEmit": true,
15 | "resolveJsonModule": true,
16 | "strict": false,
17 | "target": "es5"
18 | },
19 | "include": ["src", "next-env.d.ts"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.1.0-beta.1",
4 | "private": true,
5 | "publishConfig": {
6 | "access": "public"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx",
7 | "lib": ["ES2015", "DOM"],
8 | "module": "ESNext",
9 | "target": "es6"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/ui/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["custom/react-internal"],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/ui/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @floe/ui
2 |
3 | ## 0.1.0-beta.4
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/lib@0.1.0-beta.5
13 |
14 | ## 0.1.0-beta.3
15 |
16 | ### Minor Changes
17 |
18 | - 5f84851: Add support for token usage and pro / basic models.
19 |
20 | ### Patch Changes
21 |
22 | - Updated dependencies [5f84851]
23 | - @floe/lib@0.1.0-beta.4
24 |
25 | ## 0.1.0-beta.2
26 |
27 | ### Minor Changes
28 |
29 | - Bump to beta version.
30 |
31 | ### Patch Changes
32 |
33 | - Updated dependencies
34 | - @floe/lib@0.1.0-beta.2
35 |
36 | ## 0.1.0-alpha.1
37 |
38 | ### Patch Changes
39 |
40 | - Updated dependencies
41 | - @floe/lib@0.1.0-alpha.1
42 |
43 | ## 0.1.0-alpha.0
44 |
45 | ### Minor Changes
46 |
47 | - Create initial alpha version.
48 |
--------------------------------------------------------------------------------
/packages/ui/card.tsx:
--------------------------------------------------------------------------------
1 | interface CardProps {
2 | title?: string;
3 | className?: string;
4 | children: React.ReactNode;
5 | }
6 |
7 | export function Card({ title, className, children }: CardProps) {
8 | return (
9 |
10 | {title ? (
11 |
{title}
12 | ) : null}
13 |
{children}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/packages/ui/clipboard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { ClipboardDocumentIcon } from "@heroicons/react/24/outline";
5 | import { ClipboardDocumentCheckIcon } from "@heroicons/react/24/solid";
6 |
7 | export function Clipboard({
8 | text,
9 | className,
10 | }: {
11 | text: string;
12 | className?: string;
13 | }) {
14 | const [copied, setCopied] = useState(false);
15 |
16 | const copyToClipboard = async () => {
17 | try {
18 | await navigator.clipboard.writeText(text);
19 | setCopied(true);
20 | setTimeout(() => {
21 | setCopied(false);
22 | }, 2000);
23 | } catch (err) {
24 | console.error("Failed to copy: ", err);
25 | }
26 | };
27 |
28 | return (
29 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/packages/ui/index.ts:
--------------------------------------------------------------------------------
1 | export { Ping } from "./ping";
2 | export { Pill } from "./pill";
3 | export { Card } from "./card";
4 | export { Modal } from "./modal";
5 | export { Input } from "./input";
6 | export { Button } from "./button";
7 | export { Spinner } from "./spinner";
8 | export { Clipboard } from "./clipboard";
9 | export { Accordion } from "./accordion";
10 | export { ActionCard } from "./action-card";
11 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@floe/ui",
3 | "version": "0.1.0-beta.4",
4 | "private": true,
5 | "main": "./index.tsx",
6 | "types": "./index.tsx",
7 | "scripts": {
8 | "lint": "eslint ."
9 | },
10 | "dependencies": {
11 | "@floe/lib": "workspace:*",
12 | "@headlessui/react": "^1.7.16",
13 | "@heroicons/react": "^2.0.18",
14 | "@radix-ui/react-accordion": "^1.1.2",
15 | "@radix-ui/react-popover": "^1.0.7"
16 | },
17 | "devDependencies": {
18 | "@floe/tailwind": "workspace:*",
19 | "@turbo/gen": "^1.10.12",
20 | "@types/node": "^20.5.2",
21 | "@types/react": "^18.2.0",
22 | "@types/react-dom": "^18.2.0",
23 | "autoprefixer": "^10.4.16",
24 | "classnames": "^2.3.2",
25 | "eslint-config-custom": "workspace:*",
26 | "postcss": "^8.4.31",
27 | "react": "^18.2.0",
28 | "tailwindcss": "^3.3.3",
29 | "tsconfig": "workspace:*",
30 | "typescript": "^4.5.2"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/ui/pill.tsx:
--------------------------------------------------------------------------------
1 | import cn from "classnames";
2 |
3 | const colors = {
4 | red: "bg-red-100 text-red-700",
5 | gray: "bg-gray-100 text-gray-700",
6 | green: "bg-green-100 text-green-700",
7 | amber: "bg-amber-100 text-amber-700",
8 | yellow: "bg-yellow-100 text-yellow-700",
9 | black: "bg-zinc-900 text-white",
10 | };
11 |
12 | interface PillProps {
13 | text: string;
14 | fontStlye?: "sans" | "mono";
15 | color?: keyof typeof colors;
16 | }
17 |
18 | export function Pill({ text, color = "amber", fontStlye = "sans" }: PillProps) {
19 | return (
20 |
29 | {text}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/ui/ping.tsx:
--------------------------------------------------------------------------------
1 | export function Ping() {
2 | return (
3 |
4 |
5 |
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/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 |
--------------------------------------------------------------------------------
/packages/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { classNames } from "@floe/lib/class-names";
2 |
3 | export function Spinner({
4 | size = "md",
5 | color = "amber",
6 | }: {
7 | size?: "sm" | "md" | "lg";
8 | color?: "amber" | "zinc";
9 | }) {
10 | const sizes = {
11 | sm: "w-4 h-4",
12 | md: "w-5 h-5",
13 | lg: "w-8 h-8",
14 | };
15 |
16 | const colors = {
17 | amber: "text-amber-200 fill-amber-800",
18 | zinc: "text-zinc-200 fill-zinc-600",
19 | };
20 |
21 | return (
22 |
23 |
39 |
Loading...
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/packages/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // tailwind config is required for editor support
2 | import sharedConfig from "@floe/tailwind/tailwind.config.js";
3 |
4 | const config = {
5 | presets: [sharedConfig],
6 | };
7 |
8 | // eslint-disable-next-line import/no-default-export -- tailwind default export required
9 | export default config;
10 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "include": ["."],
4 | "exclude": ["dist", "build", "node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "apps/*"
3 | - "actions/*"
4 | - "packages/*"
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "globalDependencies": ["**/.env.*local"],
4 | "pipeline": {
5 | "build": {
6 | "dependsOn": ["^build", "^db:generate"],
7 | "outputs": [".next/**", "!.next/cache/**"],
8 | "env": [
9 | "PORT",
10 | "NODE_ENV",
11 | "NEXT_PUBLIC_SITE_URL",
12 | "NEXT_PUBLIC_VERCEL_URL",
13 | "VERCEL",
14 | "DATABASE_URL",
15 | "NEXTAUTH_SECRET",
16 | "NEXTAUTH_URL",
17 | "APP_ID",
18 | "PRIVATE_KEY",
19 | "GITHUB_CLIENT_ID",
20 | "GITHUB_CLIENT_SECRET",
21 | "SKIP_ENV_VALIDATION",
22 | "SENDGRID_API",
23 | "FLOE_SECRET_KEY",
24 | "FLOE_SECRET_IV",
25 | "OPENAI_API_KEY",
26 | "PINECONE_API_KEY",
27 | "PINCECONE_INDEX",
28 | "STRIPE_SECRET_KEY",
29 | "STRIPE_WEBHOOK_SECRET",
30 | "STRIPE_PRO_PRICE_ID",
31 | "LANGFUSE_PUBLIC_KEY",
32 | "LANGFUSE_SECRET_KEY"
33 | ]
34 | },
35 | "lint": {},
36 | "dev": {
37 | "cache": false,
38 | "persistent": true
39 | },
40 | "db:generate": {
41 | "cache": false
42 | },
43 | "db:push": {
44 | "cache": false
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------