├── apps ├── web │ ├── public │ │ ├── demo.gif │ │ ├── favicon.ico │ │ ├── _routes.json │ │ └── _headers │ ├── .gitignore │ ├── resources │ │ ├── screenshot.jpg │ │ ├── tiktoken_bg.wasm │ │ └── gitmojis.json │ ├── postcss.config.js │ ├── functions │ │ └── [[path]].ts │ ├── components.json │ ├── app │ │ ├── entry.client.tsx │ │ ├── routes │ │ │ ├── sitemap[.]xml.ts │ │ │ ├── robots[.]txt.ts │ │ │ └── _index.tsx │ │ ├── components │ │ │ └── ui │ │ │ │ ├── textarea.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── button.tsx │ │ │ │ └── select.tsx │ │ ├── entry.server.tsx │ │ ├── tailwind.css │ │ └── root.tsx │ ├── load-context.ts │ ├── vite.config.ts │ ├── tsconfig.json │ ├── lib │ │ └── utils.ts │ ├── package.json │ ├── .eslintrc.cjs │ └── tailwind.config.ts └── cli │ ├── package.json │ ├── Makefile │ ├── go.mod │ ├── emoji.go │ ├── genmoji.go │ ├── main.go │ ├── prompt.go │ ├── client.go │ └── go.sum ├── demo.tape ├── turbo.json ├── package.json ├── .editorconfig ├── .gitignore ├── README.md ├── scripts └── install.sh └── .github └── workflows └── ci.yml /apps/web/public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segersniels/genmoji/HEAD/apps/web/public/demo.gif -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | .env 6 | .dev.vars 7 | 8 | .wrangler 9 | -------------------------------------------------------------------------------- /apps/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli", 3 | "scripts": { 4 | "build": "make build" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segersniels/genmoji/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/resources/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segersniels/genmoji/HEAD/apps/web/resources/screenshot.jpg -------------------------------------------------------------------------------- /apps/web/resources/tiktoken_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segersniels/genmoji/HEAD/apps/web/resources/tiktoken_bg.wasm -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/public/_routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "include": ["/*"], 4 | "exclude": ["/favicon.ico", "/assets/*"] 5 | } 6 | -------------------------------------------------------------------------------- /demo.tape: -------------------------------------------------------------------------------- 1 | Output demo.gif 2 | 3 | Require echo 4 | 5 | Set Shell "bash" 6 | Set FontSize 16 7 | Set Width 800 8 | Set Height 400 9 | 10 | Type `genmoji commit` Sleep 500ms Enter 11 | Sleep 5s Enter 12 | Sleep 5s Left Sleep 500ms Enter 13 | Sleep 1s 14 | -------------------------------------------------------------------------------- /apps/web/public/_headers: -------------------------------------------------------------------------------- 1 | https://:project.pages.dev/* 2 | X-Robots-Tag: noindex 3 | 4 | /demo.gif 5 | Cache-Control: public, max-age=2592000, s-maxage=2592000 6 | /favicon.ico 7 | Cache-Control: public, max-age=3600, s-maxage=3600 8 | /assets/* 9 | Cache-Control: public, max-age=31536000, immutable 10 | -------------------------------------------------------------------------------- /apps/web/functions/[[path]].ts: -------------------------------------------------------------------------------- 1 | import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore - the server build file is generated by `remix vite:build` 5 | // eslint-disable-next-line import/no-unresolved 6 | import * as build from "../build/server"; 7 | 8 | export const onRequest = createPagesFunctionHandler({ build }); 9 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**", ".next/**", "zig-cache/**", "zig-out/**"] 7 | }, 8 | "build:ci": { 9 | "dependsOn": ["^build:ci"], 10 | "outputs": ["dist/**", ".next/**", "zig-cache/**", "zig-out/**"] 11 | }, 12 | "lint": {}, 13 | "lint:fix": {} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/tailwind.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "components", 15 | "utils": "lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genmoji", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm run dev --workspace web", 7 | "build": "turbo build", 8 | "build:ci": "turbo build:ci", 9 | "lint": "turbo lint", 10 | "lint:fix": "turbo lint:fix" 11 | }, 12 | "devDependencies": { 13 | "turbo": "^1.13.0" 14 | }, 15 | "workspaces": [ 16 | "apps/*", 17 | "packages/*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Default ruleset for all files 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | charset = utf-8 12 | indent_style = space 13 | indent_size = 2 14 | quote_type = single 15 | 16 | # Zig ruleset 17 | [*.zig] 18 | indent_size = 4 19 | 20 | # Makefile 21 | [Makefile] 22 | indent_style = tab 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # turbo 39 | .turbo 40 | 41 | # zig 42 | zig-cache/ 43 | zig-out/ 44 | bin/ 45 | -------------------------------------------------------------------------------- /apps/web/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/web/load-context.ts: -------------------------------------------------------------------------------- 1 | import { type PlatformProxy } from 'wrangler'; 2 | 3 | // When using `wrangler.toml` to configure bindings, 4 | // `wrangler types` will generate types for those bindings 5 | // into the global `Env` interface. 6 | // Need this empty interface so that typechecking passes 7 | // even if no `wrangler.toml` exists. 8 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 9 | interface Env { 10 | OPENAI_API_KEY: string; 11 | } 12 | 13 | type Cloudflare = Omit, 'dispose'>; 14 | 15 | declare module '@remix-run/cloudflare' { 16 | interface AppLoadContext { 17 | cloudflare: Cloudflare; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | vitePlugin as remix, 3 | cloudflareDevProxyVitePlugin as remixCloudflareDevProxy, 4 | } from '@remix-run/dev'; 5 | import { defineConfig } from 'vite'; 6 | import tsconfigPaths from 'vite-tsconfig-paths'; 7 | 8 | export default defineConfig({ 9 | server: { 10 | port: 3000, 11 | }, 12 | plugins: [ 13 | remixCloudflareDevProxy(), 14 | remix({ 15 | future: { 16 | v3_fetcherPersist: true, 17 | v3_relativeSplatPath: true, 18 | v3_throwAbortReason: true, 19 | unstable_singleFetch: true, 20 | unstable_lazyRouteDiscovery: true, 21 | }, 22 | }), 23 | tsconfigPaths(), 24 | ], 25 | }); 26 | -------------------------------------------------------------------------------- /apps/cli/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean dev version demo $(TARGETS) 2 | 3 | BINARY_NAME := genmoji 4 | VERSION := 1.2.0 5 | BUILD_DIR := bin 6 | 7 | TARGETS := darwin-arm64 darwin-amd64 linux-arm64 linux-amd64 8 | LDFLAGS := -w -s -X main.AppVersion=$(VERSION) -X main.AppName=$(BINARY_NAME) 9 | 10 | build: $(TARGETS) 11 | 12 | $(TARGETS): 13 | GOOS=$(word 1,$(subst -, ,$@)) GOARCH=$(word 2,$(subst -, ,$@)) go build -o $(BUILD_DIR)/$(BINARY_NAME)-$@ -ldflags "$(LDFLAGS)" 14 | 15 | clean: 16 | rm -rf $(BUILD_DIR) 17 | 18 | dev: 19 | go build -o $(BUILD_DIR)/$(BINARY_NAME) -ldflags "$(LDFLAGS)" 20 | 21 | version: 22 | @echo $(VERSION) 23 | 24 | demo: 25 | @vhs ../../demo.tape -o ../web/public/demo.gif 26 | -------------------------------------------------------------------------------- /apps/web/app/routes/sitemap[.]xml.ts: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs } from '@remix-run/cloudflare'; 2 | 3 | export async function loader({ request }: LoaderFunctionArgs) { 4 | const url = new URL(request.url); 5 | const baseUrl = `${url.protocol}//${url.host}`; 6 | 7 | const sitemap = ` 8 | 9 | ${baseUrl} 10 | ${baseUrl}/web 11 | 12 | `; 13 | 14 | return new Response(sitemap, { 15 | status: 200, 16 | headers: { 17 | 'Content-Type': 'application/xml', 18 | 'Cache-Control': 'public, max-age=86400, must-revalidate', 19 | }, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from 'lib/utils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 | 19 | ); 20 | } 21 | ); 22 | Textarea.displayName = 'Textarea'; 23 | 24 | export { Textarea }; 25 | -------------------------------------------------------------------------------- /apps/web/app/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import * as React from 'react'; 3 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 4 | 5 | import { cn } from 'lib/utils'; 6 | 7 | const Separator = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >( 11 | ( 12 | { className, orientation = 'horizontal', decorative = true, ...props }, 13 | ref 14 | ) => ( 15 | 26 | ) 27 | ); 28 | Separator.displayName = SeparatorPrimitive.Root.displayName; 29 | 30 | export { Separator }; 31 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.ts", 4 | "**/*.tsx", 5 | "**/.server/**/*.ts", 6 | "**/.server/**/*.tsx", 7 | "**/.client/**/*.ts", 8 | "**/.client/**/*.tsx" 9 | ], 10 | "compilerOptions": { 11 | "lib": ["DOM", "DOM.Iterable", "ES2022"], 12 | "types": ["@remix-run/cloudflare", "vite/client"], 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "jsx": "react-jsx", 16 | "module": "ESNext", 17 | "moduleResolution": "Bundler", 18 | "resolveJsonModule": true, 19 | "target": "ES2022", 20 | "strict": true, 21 | "allowJs": true, 22 | "skipLibCheck": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "*": ["./app/*"], 27 | "resources/*": ["./resources/*"], 28 | "lib/*": ["./lib/*"] 29 | }, 30 | 31 | // Vite takes care of building everything, not tsc. 32 | "noEmit": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/app/routes/robots[.]txt.ts: -------------------------------------------------------------------------------- 1 | import { LoaderFunctionArgs } from '@remix-run/cloudflare'; 2 | 3 | enum PolicyType { 4 | UserAgent = 'User-agent', 5 | Allow = 'Allow', 6 | Disallow = 'Disallow', 7 | Sitemap = 'Sitemap', 8 | CrawlDelay = 'Crawl-delay', 9 | } 10 | 11 | type Policy = { 12 | type: PolicyType; 13 | value: string; 14 | }; 15 | 16 | function getRobotsText(policies: Policy[]): string { 17 | let result = ''; 18 | for (const { type, value } of policies) { 19 | result += `${type}: ${value}\n`; 20 | } 21 | 22 | return result; 23 | } 24 | 25 | export async function loader({ request }: LoaderFunctionArgs) { 26 | const url = new URL(request.url); 27 | const baseUrl = `${url.protocol}//${url.host}`; 28 | const robots = getRobotsText([ 29 | { 30 | type: PolicyType.UserAgent, 31 | value: '*', 32 | }, 33 | { 34 | type: PolicyType.Allow, 35 | value: '/', 36 | }, 37 | { 38 | type: PolicyType.Sitemap, 39 | value: `${baseUrl}/sitemap.xml`, 40 | }, 41 | ]); 42 | 43 | return new Response(robots, { 44 | headers: { 45 | 'Content-Type': 'text/plain', 46 | 'Cache-Control': 'public, max-age=86400, must-revalidate', 47 | 'Content-Length': new TextEncoder().encode(robots).byteLength.toString(), 48 | }, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # genmoji 2 | 3 | [Genmoji](https://genmoji.dev) is a tool that uses OpenAI's GPT or Anthropic's Claude API to generate commit messages from code snippets or `git diff`. With [Genmoji](https://genmoji.dev), developers can easily create informative and concise commit messages, using the [gitmoji](https://gitmoji.dev) standard, without spending time and effort writing them themselves. 4 | 5 |  6 | 7 | ## Install 8 | 9 | Bring your own API key. You will need either an OpenAI or Anthropic API key exported as the environment variables `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. 10 | 11 | ```bash 12 | # Install in the current directory 13 | curl -sSL https://raw.githubusercontent.com/segersniels/genmoji/master/scripts/install.sh | bash 14 | # Install in /usr/local/bin 15 | curl -sSL https://raw.githubusercontent.com/segersniels/genmoji/master/scripts/install.sh | sudo bash -s /usr/local/bin 16 | ``` 17 | 18 | ### Update 19 | 20 | When an update is available, you will see a message in your terminal notifying you that a new version is available. 21 | To update, run above install script again and you're good to go. 22 | 23 | ### Manual 24 | 25 | 1. Download the latest binary from the [releases](https://github.com/segersniels/genmoji/releases/latest) page for your system 26 | 2. Rename the binary to `genmoji` 27 | 3. Copy the binary to a location in your `$PATH` 28 | -------------------------------------------------------------------------------- /apps/web/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@remix-run/react'; 2 | import { buttonVariants } from 'components/ui/button'; 3 | import { cn } from 'lib/utils'; 4 | 5 | export default function Index() { 6 | return ( 7 | 8 | 9 | 10 | Generate gitmoji commit messages 11 | 12 | 13 | 14 | 15 | Why spend time thinking about commit messages when you can generate 16 | them using AI? 17 | 18 | 19 | 20 | 21 | 22 | 31 | Download 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Base URL for downloading binaries 4 | BASE_URL="https://github.com/segersniels/genmoji/releases/latest/download" 5 | 6 | # Default destination directory (current directory) 7 | DEST_DIR="." 8 | 9 | # Check if a command-line argument was provided for the destination 10 | if [ "$#" -eq 1 ]; then 11 | # If a destination directory is provided, use it 12 | DEST_DIR="$1" 13 | fi 14 | 15 | # Identify OS and Architecture 16 | OS="$(uname -s)" 17 | ARCH="$(uname -m)" 18 | 19 | case "${OS}" in 20 | Linux*) os=linux ;; 21 | Darwin*) os=darwin ;; 22 | *) 23 | echo "Unsupported OS. Exiting..." 24 | exit 1 25 | ;; 26 | esac 27 | 28 | case "${ARCH}" in 29 | x86_64*) arch=amd64 ;; 30 | arm64*) arch=arm64 ;; 31 | *) 32 | echo "Unsupported architecture. Exiting..." 33 | exit 1 34 | ;; 35 | esac 36 | 37 | # Construct binary name and download URL 38 | BIN_NAME="genmoji-${os}-${arch}" 39 | DOWNLOAD_URL="${BASE_URL}/${BIN_NAME}" 40 | 41 | # Full path to the target binary 42 | FULL_PATH="${DEST_DIR}/genmoji" 43 | 44 | echo "Downloading ${BIN_NAME} to ${FULL_PATH}..." 45 | 46 | # Download the binary 47 | if command -v curl >/dev/null; then 48 | curl -L -o "${FULL_PATH}" "${DOWNLOAD_URL}" 49 | elif command -v wget >/dev/null; then 50 | wget -O "${FULL_PATH}" "${DOWNLOAD_URL}" 51 | else 52 | echo "Error: curl or wget is required to download the binary." 53 | exit 1 54 | fi 55 | 56 | # Make the binary executable 57 | chmod +x "${FULL_PATH}" 58 | echo "Download completed. The binary is available at ${FULL_PATH}" 59 | -------------------------------------------------------------------------------- /apps/web/app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle generating the HTTP Response for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.server 5 | */ 6 | 7 | import type { AppLoadContext, EntryContext } from '@remix-run/cloudflare'; 8 | import { RemixServer } from '@remix-run/react'; 9 | import { isbot } from 'isbot'; 10 | import { renderToReadableStream } from 'react-dom/server'; 11 | 12 | export default async function handleRequest( 13 | request: Request, 14 | responseStatusCode: number, 15 | responseHeaders: Headers, 16 | remixContext: EntryContext, 17 | // This is ignored so we can keep it in the template for visibility. Feel 18 | // free to delete this parameter in your app if you're not using it! 19 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 20 | loadContext: AppLoadContext 21 | ) { 22 | const body = await renderToReadableStream( 23 | , 24 | { 25 | signal: request.signal, 26 | onError(error: unknown) { 27 | // Log streaming rendering errors from inside the shell 28 | console.error(error); 29 | responseStatusCode = 500; 30 | }, 31 | } 32 | ); 33 | 34 | if (isbot(request.headers.get('user-agent') || '')) { 35 | await body.allReady; 36 | } 37 | 38 | responseHeaders.set('Content-Type', 'text/html'); 39 | return new Response(body, { 40 | headers: responseHeaders, 41 | status: responseStatusCode, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { MetaArgs } from '@remix-run/cloudflare'; 2 | import { type ClassValue, clsx } from 'clsx'; 3 | import { twMerge } from 'tailwind-merge'; 4 | import { type ServerRuntimeMetaDescriptor } from '@remix-run/server-runtime'; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | 10 | /** 11 | * Extends the metadata of a route by combining parent metadata with new metadata. 12 | * This function merges metadata from parent routes with the current route's metadata, 13 | * ensuring that new entries override existing ones with the same title, name, or property. 14 | * 15 | * @param matches - An array of route matches from Remix's MetaArgs 16 | * @param meta - An array of new metadata descriptors to be added or updated 17 | * @returns A combined array of metadata descriptors 18 | */ 19 | export function extendMeta( 20 | matches: MetaArgs['matches'], 21 | meta: ServerRuntimeMetaDescriptor[] 22 | ): ServerRuntimeMetaDescriptor[] { 23 | const parentMeta: ServerRuntimeMetaDescriptor[] = matches 24 | .flatMap((match) => match.meta ?? []) 25 | .filter( 26 | (item) => 27 | !meta.some((metaItem) => { 28 | const hasSameTitle = 'title' in item && 'title' in metaItem; 29 | const hasSameName = 30 | 'name' in item && 'name' in metaItem && item.name === metaItem.name; 31 | const hasSameProperty = 32 | 'property' in item && 33 | 'property' in metaItem && 34 | item.property === metaItem.property; 35 | 36 | return hasSameTitle || hasSameName || hasSameProperty; 37 | }) 38 | ); 39 | 40 | return [...parentMeta, ...meta]; 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | concurrency: 10 | group: github.ref 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-go@v5 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '20.x' 22 | registry-url: 'https://registry.npmjs.org' 23 | - run: npm install 24 | - run: npm run lint 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v4 27 | with: 28 | working-directory: apps/cli 29 | 30 | build: 31 | needs: [lint] 32 | runs-on: ubuntu-latest 33 | if: github.ref == 'refs/heads/master' 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-go@v5 37 | - uses: actions/setup-node@v4 38 | with: 39 | node-version: '20.x' 40 | registry-url: 'https://registry.npmjs.org' 41 | - run: npm install 42 | - run: npm run build --workspace cli 43 | - name: Get current package version 44 | id: current_version 45 | run: echo "version=$(make version)" >> $GITHUB_OUTPUT 46 | working-directory: apps/cli 47 | - uses: mukunku/tag-exists-action@v1.4.0 48 | id: check_tag 49 | with: 50 | tag: '${{ steps.current_version.outputs.version }}' 51 | - uses: ncipollo/release-action@v1 52 | if: steps.check_tag.outputs.exists == 'false' 53 | with: 54 | tag: '${{ steps.current_version.outputs.version }}' 55 | artifacts: 'apps/cli/bin/*' 56 | generateReleaseNotes: true 57 | makeLatest: true 58 | prerelease: false 59 | replacesArtifacts: true 60 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "private": true, 5 | "sideEffects": false, 6 | "type": "module", 7 | "scripts": { 8 | "build": "remix vite:build", 9 | "deploy": "wrangler pages deploy ./build/client", 10 | "dev": "remix vite:dev", 11 | "lint": "eslint .", 12 | "lint:fix": "eslint --fix .", 13 | "start": "wrangler pages dev ./build/client", 14 | "typecheck": "tsc", 15 | "typegen": "wrangler types" 16 | }, 17 | "dependencies": { 18 | "@radix-ui/react-icons": "^1.3.0", 19 | "@radix-ui/react-select": "^2.1.1", 20 | "@radix-ui/react-separator": "^1.1.0", 21 | "@radix-ui/react-slot": "^1.1.0", 22 | "@remix-run/cloudflare": "^2.11.0", 23 | "@remix-run/cloudflare-pages": "^2.11.0", 24 | "@remix-run/react": "^2.11.0", 25 | "class-variance-authority": "^0.7.0", 26 | "clsx": "^2.1.1", 27 | "isbot": "^4.1.0", 28 | "localforage": "^1.10.0", 29 | "lucide-react": "^0.424.0", 30 | "openai": "^4.54.0", 31 | "react": "^18.2.0", 32 | "react-dom": "^18.2.0", 33 | "tailwind-merge": "^2.4.0", 34 | "tailwindcss-animate": "^1.0.7" 35 | }, 36 | "devDependencies": { 37 | "@cloudflare/workers-types": "^4.20240512.0", 38 | "@remix-run/dev": "^2.11.0", 39 | "@types/react": "^18.2.20", 40 | "@types/react-dom": "^18.2.7", 41 | "@typescript-eslint/eslint-plugin": "^6.7.4", 42 | "@typescript-eslint/parser": "^6.7.4", 43 | "autoprefixer": "^10.4.19", 44 | "eslint": "^8.38.0", 45 | "eslint-import-resolver-typescript": "^3.6.1", 46 | "eslint-plugin-import": "^2.28.1", 47 | "eslint-plugin-jsx-a11y": "^6.7.1", 48 | "eslint-plugin-react": "^7.33.2", 49 | "eslint-plugin-react-hooks": "^4.6.0", 50 | "postcss": "^8.4.38", 51 | "tailwindcss": "^3.4.4", 52 | "typescript": "^5.1.6", 53 | "vite": "^5.1.0", 54 | "vite-tsconfig-paths": "^4.2.1", 55 | "wrangler": "3.57.1" 56 | }, 57 | "engines": { 58 | "node": ">=20.0.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/cli/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/segersniels/genmoji 2 | 3 | go 1.22.5 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/atotto/clipboard v0.1.4 // indirect 9 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 10 | github.com/catppuccin/go v0.2.0 // indirect 11 | github.com/charmbracelet/bubbles v0.18.0 // indirect 12 | github.com/charmbracelet/bubbletea v0.25.0 // indirect 13 | github.com/charmbracelet/huh v0.3.0 // indirect 14 | github.com/charmbracelet/huh/spinner v0.0.0-20240417163504-acfe24c3f5b5 // indirect 15 | github.com/charmbracelet/lipgloss v0.10.0 // indirect 16 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 17 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect 18 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 19 | github.com/go-logfmt/logfmt v0.6.0 // indirect 20 | github.com/hashicorp/go-version v1.7.0 // indirect 21 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/mattn/go-localereader v0.0.1 // indirect 24 | github.com/mattn/go-runewidth v0.0.15 // indirect 25 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 26 | github.com/muesli/cancelreader v0.2.2 // indirect 27 | github.com/muesli/reflow v0.3.0 // indirect 28 | github.com/muesli/termenv v0.15.2 // indirect 29 | github.com/rivo/uniseg v0.4.7 // indirect 30 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 31 | github.com/sashabaranov/go-openai v1.24.0 // indirect 32 | github.com/segersniels/config v0.0.0-20240503115636-403023c44d9f // indirect 33 | github.com/segersniels/updater v1.2.1 // indirect 34 | github.com/urfave/cli/v2 v2.27.1 // indirect 35 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 36 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 37 | golang.org/x/sync v0.6.0 // indirect 38 | golang.org/x/sys v0.16.0 // indirect 39 | golang.org/x/term v0.16.0 // indirect 40 | golang.org/x/text v0.14.0 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /apps/web/app/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 20 14.3% 4.1%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 20 14.3% 4.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 20 14.3% 4.1%; 13 | --primary: 24 9.8% 10%; 14 | --primary-foreground: 60 9.1% 97.8%; 15 | --secondary: 60 4.8% 95.9%; 16 | --secondary-foreground: 24 9.8% 10%; 17 | --muted: 60 4.8% 95.9%; 18 | --muted-foreground: 25 5.3% 44.7%; 19 | --accent: 60 4.8% 95.9%; 20 | --accent-foreground: 24 9.8% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 60 9.1% 97.8%; 23 | --border: 20 5.9% 90%; 24 | --input: 20 5.9% 90%; 25 | --ring: 20 14.3% 4.1%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 20 14.3% 4.1%; 36 | --foreground: 60 9.1% 97.8%; 37 | --card: 20 14.3% 4.1%; 38 | --card-foreground: 60 9.1% 97.8%; 39 | --popover: 20 14.3% 4.1%; 40 | --popover-foreground: 60 9.1% 97.8%; 41 | --primary: 60 9.1% 97.8%; 42 | --primary-foreground: 24 9.8% 10%; 43 | --secondary: 12 6.5% 15.1%; 44 | --secondary-foreground: 60 9.1% 97.8%; 45 | --muted: 12 6.5% 15.1%; 46 | --muted-foreground: 24 5.4% 63.9%; 47 | --accent: 12 6.5% 15.1%; 48 | --accent-foreground: 60 9.1% 97.8%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 60 9.1% 97.8%; 51 | --border: 12 6.5% 15.1%; 52 | --input: 12 6.5% 15.1%; 53 | --ring: 24 5.7% 82.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } -------------------------------------------------------------------------------- /apps/web/app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from 'lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 14 | destructive: 15 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 16 | outline: 17 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 18 | secondary: 19 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 20 | ghost: 'hover:bg-accent hover:text-accent-foreground', 21 | link: 'text-primary underline-offset-4 hover:underline', 22 | }, 23 | size: { 24 | default: 'h-9 px-4 py-2', 25 | sm: 'h-8 rounded-md px-3 text-xs', 26 | lg: 'h-10 rounded-md px-8', 27 | icon: 'h-9 w-9', 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: 'default', 32 | size: 'default', 33 | }, 34 | } 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : 'button'; 46 | return ( 47 | 52 | ); 53 | } 54 | ); 55 | Button.displayName = 'Button'; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /apps/cli/emoji.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type Gitmoji struct { 12 | Emoji string `json:"emoji"` 13 | Code string `json:"code"` 14 | Description string `json:"description"` 15 | Name string `json:"name"` 16 | } 17 | 18 | type Response struct { 19 | Gitmojis []Gitmoji `json:"gitmojis"` 20 | } 21 | 22 | func isCached(path string) bool { 23 | if _, err := os.Stat(path); os.IsNotExist(err) { 24 | return false 25 | } 26 | 27 | return true 28 | } 29 | 30 | func fetchFromCache(path string) ([]Gitmoji, error) { 31 | file, err := os.Open(path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | defer file.Close() 36 | 37 | var response Response 38 | err = json.NewDecoder(file).Decode(&response) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return response.Gitmojis, nil 44 | } 45 | 46 | func writeToCache(path string, response Response) error { 47 | directory := filepath.Dir(path) 48 | if _, err := os.Stat(directory); os.IsNotExist(err) { 49 | err := os.MkdirAll(directory, os.ModePerm) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | 55 | file, err := os.Create(path) 56 | if err != nil { 57 | return err 58 | } 59 | defer file.Close() 60 | 61 | err = json.NewEncoder(file).Encode(&response) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func fetchGitmojis() ([]Gitmoji, error) { 70 | dirname, err := os.UserHomeDir() 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | path := filepath.Join(dirname, ".genmoji", "gitmojis.json") 76 | if isCached(path) { 77 | return fetchFromCache(path) 78 | } 79 | 80 | res, err := http.Get("https://gitmoji.dev/api/gitmojis") 81 | if err != nil { 82 | return nil, err 83 | } 84 | defer res.Body.Close() 85 | 86 | var response Response 87 | if err := json.NewDecoder(res.Body).Decode(&response); err != nil { 88 | return nil, err 89 | } 90 | 91 | if err := writeToCache(path, response); err != nil { 92 | return nil, err 93 | } 94 | 95 | return response.Gitmojis, nil 96 | } 97 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is intended to be a basic starting point for linting in your app. 3 | * It relies on recommended configs out of the box for simplicity, but you can 4 | * and should modify this configuration to best suit your team's needs. 5 | */ 6 | 7 | /** @type {import('eslint').Linter.Config} */ 8 | module.exports = { 9 | root: true, 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | sourceType: 'module', 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | env: { 18 | browser: true, 19 | commonjs: true, 20 | es6: true, 21 | }, 22 | ignorePatterns: ['!**/.server', '!**/.client'], 23 | 24 | // Base config 25 | extends: ['eslint:recommended'], 26 | 27 | overrides: [ 28 | // React 29 | { 30 | files: ['**/*.{js,jsx,ts,tsx}'], 31 | plugins: ['react', 'jsx-a11y'], 32 | extends: [ 33 | 'plugin:react/recommended', 34 | 'plugin:react/jsx-runtime', 35 | 'plugin:react-hooks/recommended', 36 | 'plugin:jsx-a11y/recommended', 37 | ], 38 | settings: { 39 | react: { 40 | version: 'detect', 41 | }, 42 | formComponents: ['Form'], 43 | linkComponents: [ 44 | { name: 'Link', linkAttribute: 'to' }, 45 | { name: 'NavLink', linkAttribute: 'to' }, 46 | ], 47 | 'import/resolver': { 48 | typescript: { 49 | project: 'apps/web/tsconfig.json', 50 | }, 51 | }, 52 | }, 53 | }, 54 | 55 | // Typescript 56 | { 57 | files: ['**/*.{ts,tsx}'], 58 | plugins: ['@typescript-eslint', 'import'], 59 | parser: '@typescript-eslint/parser', 60 | settings: { 61 | 'import/internal-regex': '^~/', 62 | 'import/resolver': { 63 | node: { 64 | extensions: ['.ts', '.tsx'], 65 | }, 66 | typescript: { 67 | project: 'apps/web/tsconfig.json', 68 | alwaysTryTypes: true, 69 | }, 70 | }, 71 | }, 72 | extends: [ 73 | 'plugin:@typescript-eslint/recommended', 74 | 'plugin:import/recommended', 75 | 'plugin:import/typescript', 76 | ], 77 | }, 78 | 79 | // Node 80 | { 81 | files: ['.eslintrc.cjs'], 82 | env: { 83 | node: true, 84 | }, 85 | }, 86 | ], 87 | }; 88 | -------------------------------------------------------------------------------- /apps/cli/genmoji.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | 8 | "github.com/charmbracelet/huh" 9 | "github.com/charmbracelet/huh/spinner" 10 | "github.com/charmbracelet/lipgloss" 11 | ) 12 | 13 | type Genmoji struct { 14 | client MessageClient 15 | } 16 | 17 | func NewGenmoji() *Genmoji { 18 | var ( 19 | client MessageClient 20 | apiKey string 21 | ) 22 | 23 | // Depending on the user selected model, we need to set the corresponding API key 24 | switch CONFIG.Data.Model { 25 | case Claude3Dot7Sonnet: 26 | case Claude3Dot5Sonnet: 27 | case Claude3Dot5Haiku: 28 | apiKey = os.Getenv("ANTHROPIC_API_KEY") 29 | if apiKey == "" { 30 | log.Fatal("ANTHROPIC_API_KEY is not set") 31 | } 32 | 33 | client = NewAnthropic(apiKey, CONFIG.Data.Model) 34 | default: 35 | apiKey = os.Getenv("OPENAI_API_KEY") 36 | if apiKey == "" { 37 | log.Fatal("OPENAI_API_KEY is not set") 38 | } 39 | 40 | client = NewOpenAI(apiKey, CONFIG.Data.Model) 41 | } 42 | 43 | return &Genmoji{ 44 | client, 45 | } 46 | } 47 | 48 | func (g *Genmoji) Generate() (string, error) { 49 | diff, err := getStagedChanges() 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | gitmojis, err := fetchGitmojis() 55 | if err != nil { 56 | return "", err 57 | } 58 | 59 | list, err := json.Marshal(gitmojis) 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | var response string 65 | if err := spinner.New().TitleStyle(lipgloss.NewStyle()).Title("Generating your commit message...").Action(func() { 66 | response, err = g.client.CreateMessage(diff, list) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | }).Run(); err != nil { 71 | return "", err 72 | } 73 | 74 | return response, nil 75 | } 76 | 77 | func (g *Genmoji) Commit() error { 78 | var response string 79 | var err error 80 | for { 81 | response, err = g.Generate() 82 | if err != nil { 83 | return err 84 | } 85 | 86 | var confirmation bool 87 | err = huh.NewConfirm().Title(response).Description("Do you want to commit this message?").Value(&confirmation).Run() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if confirmation { 93 | break 94 | } 95 | } 96 | 97 | if err := commit(response); err != nil { 98 | return err 99 | } 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: '', 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: '2rem', 16 | screens: { 17 | '2xl': '1400px', 18 | }, 19 | }, 20 | extend: { 21 | fontFamily: { 22 | sans: ['Inter', 'sans-serif'], 23 | }, 24 | colors: { 25 | border: 'hsl(var(--border))', 26 | input: 'hsl(var(--input))', 27 | ring: 'hsl(var(--ring))', 28 | background: 'hsl(var(--background))', 29 | foreground: 'hsl(var(--foreground))', 30 | primary: { 31 | DEFAULT: 'hsl(var(--primary))', 32 | foreground: 'hsl(var(--primary-foreground))', 33 | }, 34 | secondary: { 35 | DEFAULT: 'hsl(var(--secondary))', 36 | foreground: 'hsl(var(--secondary-foreground))', 37 | }, 38 | destructive: { 39 | DEFAULT: 'hsl(var(--destructive))', 40 | foreground: 'hsl(var(--destructive-foreground))', 41 | }, 42 | muted: { 43 | DEFAULT: 'hsl(var(--muted))', 44 | foreground: 'hsl(var(--muted-foreground))', 45 | }, 46 | accent: { 47 | DEFAULT: 'hsl(var(--accent))', 48 | foreground: 'hsl(var(--accent-foreground))', 49 | }, 50 | popover: { 51 | DEFAULT: 'hsl(var(--popover))', 52 | foreground: 'hsl(var(--popover-foreground))', 53 | }, 54 | card: { 55 | DEFAULT: 'hsl(var(--card))', 56 | foreground: 'hsl(var(--card-foreground))', 57 | }, 58 | }, 59 | borderRadius: { 60 | lg: 'var(--radius)', 61 | md: 'calc(var(--radius) - 2px)', 62 | sm: 'calc(var(--radius) - 4px)', 63 | }, 64 | keyframes: { 65 | 'accordion-down': { 66 | from: { height: '0' }, 67 | to: { height: 'var(--radix-accordion-content-height)' }, 68 | }, 69 | 'accordion-up': { 70 | from: { height: 'var(--radix-accordion-content-height)' }, 71 | to: { height: '0' }, 72 | }, 73 | }, 74 | animation: { 75 | 'accordion-down': 'accordion-down 0.2s ease-out', 76 | 'accordion-up': 'accordion-up 0.2s ease-out', 77 | }, 78 | }, 79 | }, 80 | plugins: [require('tailwindcss-animate')], 81 | } satisfies Config; 82 | 83 | export default config; 84 | -------------------------------------------------------------------------------- /apps/web/app/root.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | Links, 4 | Meta, 5 | Outlet, 6 | Scripts, 7 | ScrollRestoration, 8 | useRouteError, 9 | } from '@remix-run/react'; 10 | import './tailwind.css'; 11 | import { MetaFunction } from '@remix-run/cloudflare'; 12 | 13 | export const meta: MetaFunction = ({ error }) => { 14 | if (error) { 15 | return [{ title: 'Oops, something went wrong!' }]; 16 | } 17 | 18 | const title = 'Genmoji'; 19 | const description = 20 | 'Generate your gitmoji commit message using AI. Provide a git diff and let Genmoji do the work for you.'; 21 | 22 | return [ 23 | { title }, 24 | { name: 'description', content: description }, 25 | { name: 'og:title', content: title }, 26 | { name: 'og:description', content: description }, 27 | { name: 'twitter:title', content: title }, 28 | { name: 'twitter:description', content: description }, 29 | ]; 30 | }; 31 | 32 | export function Layout({ children }: { children: React.ReactNode }) { 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 53 | 54 | 55 | 56 | 57 | {children} 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default function App() { 68 | return ; 69 | } 70 | 71 | export function ErrorBoundary() { 72 | const error = useRouteError(); 73 | 74 | return ( 75 | 76 | 77 | Oh no! 78 | 79 | 80 | 81 | 82 | 83 | 84 | Oops, something went wrong! 85 | 86 | {JSON.stringify(error, null, 2)} 87 | 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /apps/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/charmbracelet/huh" 10 | "github.com/segersniels/config" 11 | updater "github.com/segersniels/updater" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | var ( 16 | AppVersion string 17 | AppName string 18 | ) 19 | 20 | type Model string 21 | 22 | const ( 23 | GPT4o Model = "gpt-4o" 24 | GPT4oMini Model = "gpt-4o-mini" 25 | Claude3Dot7Sonnet Model = "claude-3-7-sonnet-latest" 26 | Claude3Dot5Sonnet Model = "claude-3-5-sonnet-latest" 27 | Claude3Dot5Haiku Model = "claude-3-5-haiku-latest" 28 | ) 29 | 30 | type ConfigData struct { 31 | Model Model `json:"model"` 32 | } 33 | 34 | var CONFIG = config.NewConfig("genmoji", ConfigData{ 35 | Model: GPT4oMini, 36 | }) 37 | 38 | func main() { 39 | upd := updater.NewUpdater(AppName, AppVersion, "segersniels") 40 | version := upd.IsNewVersionAvailable() 41 | if version != nil { 42 | fmt.Printf("A new version of %s is available (%s).\n\n", AppName, version.String()) 43 | } 44 | 45 | app := &cli.App{ 46 | Name: AppName, 47 | Usage: "Generate commit messages for your staged changes", 48 | Version: AppVersion, 49 | Commands: []*cli.Command{ 50 | { 51 | Name: "generate", 52 | Usage: "Generate a commit message", 53 | Action: func(ctx *cli.Context) error { 54 | genmoji := NewGenmoji() 55 | response, err := genmoji.Generate() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | fmt.Println(response) 61 | return nil 62 | }, 63 | }, 64 | { 65 | Name: "commit", 66 | Usage: "Generate a commit message and commit it", 67 | Action: func(ctx *cli.Context) error { 68 | genmoji := NewGenmoji() 69 | return genmoji.Commit() 70 | }, 71 | }, 72 | { 73 | Name: "config", 74 | Usage: "Configure the app", 75 | Subcommands: []*cli.Command{ 76 | { 77 | Name: "init", 78 | Usage: "Initialize the config", 79 | Action: func(ctx *cli.Context) error { 80 | models := huh.NewOptions(GPT4o, GPT4oMini, Claude3Dot7Sonnet, Claude3Dot5Sonnet, Claude3Dot5Haiku) 81 | form := huh.NewForm( 82 | huh.NewGroup( 83 | huh.NewSelect[Model]().Title("Model").Description("Configure the default model").Options(models...).Value(&CONFIG.Data.Model), 84 | ), 85 | ) 86 | 87 | err := form.Run() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return CONFIG.Save() 93 | }, 94 | }, 95 | { 96 | Name: "ls", 97 | Usage: "List the current configuration", 98 | Action: func(ctx *cli.Context) error { 99 | data, err := json.MarshalIndent(CONFIG.Data, "", " ") 100 | if err != nil { 101 | return err 102 | } 103 | 104 | fmt.Println(string(data)) 105 | return nil 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | } 112 | 113 | if err := app.Run(os.Args); err != nil { 114 | log.Fatal(err) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /apps/cli/prompt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | const SYSTEM_MESSAGE string = ` 12 | You are a helpful coding assistant responsible for generating fitting commit messages. 13 | You will be provided a git diff or code snippet and you are expected to provide a suitable commit message. 14 | If a user provides anything else than you are expecting respond with a fitting message and ask for the correct input (don't include emojis in this message). 15 | 16 | When reviewing the diff or code, focus on identifying the main purpose of the changes. 17 | Describe the change to the best of your capabilities in a maximum of one short sentence on one line. 18 | 19 | When reviewing a diff, pay attention to the changed filenames and extract the context of the changes. 20 | This will help you create a more relevant and informative commit message. 21 | Here are some examples of how you can interpret some changed filenames: 22 | - Files or filepaths that reference testing are usually related to tests. 23 | - Markdown files are usually related to documentation. 24 | - Config file adjustments are usually related to configuration changes. 25 | 26 | Try to match the generated message to a fitting emoji using its description from the provided list above. 27 | So go look in the descriptions and find the one that best matches the description. 28 | 29 | Always start your commit message with a gitmoji followed by the message starting with a capital letter. 30 | Never mention filenames or function names in the message. 31 | 32 | A gitmoji commit message should look like the following: :code: Your message here 33 | 34 | Below you can find a list of available gitmojis and their descriptions. Try to look for a fitting emoji and message. 35 | Use the code representation of the emoji in the commit message. 36 | ` 37 | 38 | var FILES_TO_IGNORE = []string{ 39 | "package-lock.json", 40 | "yarn.lock", 41 | "npm-debug.log", 42 | "yarn-debug.log", 43 | "yarn-error.log", 44 | ".pnpm-debug.log", 45 | "Cargo.lock", 46 | "Gemfile.lock", 47 | "mix.lock", 48 | "Pipfile.lock", 49 | "composer.lock", 50 | "go.sum", 51 | } 52 | 53 | func splitDiffIntoChunks(diff string) []string { 54 | split := strings.Split(diff, "diff --git")[1:] 55 | for i, chunk := range split { 56 | split[i] = strings.TrimSpace(chunk) 57 | } 58 | 59 | return split 60 | } 61 | 62 | func removeLockFiles(chunks []string) []string { 63 | var wg sync.WaitGroup 64 | 65 | filtered := make(chan string) 66 | 67 | for _, chunk := range chunks { 68 | wg.Add(1) 69 | 70 | go func(chunk string) { 71 | defer wg.Done() 72 | shouldIgnore := false 73 | header := strings.Split(chunk, "\n")[0] 74 | 75 | // Check if the first line contains any of the files to ignore 76 | for _, file := range FILES_TO_IGNORE { 77 | if strings.Contains(header, file) { 78 | shouldIgnore = true 79 | } 80 | } 81 | 82 | if !shouldIgnore { 83 | filtered <- chunk 84 | } 85 | }(chunk) 86 | } 87 | 88 | go func() { 89 | wg.Wait() 90 | close(filtered) 91 | }() 92 | 93 | var result []string 94 | for chunk := range filtered { 95 | result = append(result, chunk) 96 | } 97 | 98 | return result 99 | } 100 | 101 | // Split the diff in chunks and remove any lock files to save on tokens 102 | func prepareDiff(diff string) string { 103 | chunks := splitDiffIntoChunks(diff) 104 | 105 | return strings.Join(removeLockFiles(chunks), "\n") 106 | } 107 | 108 | func getStagedChanges() (string, error) { 109 | cmd := exec.Command("git", "diff", "--cached") 110 | stdout, err := cmd.Output() 111 | 112 | if err != nil { 113 | return "", err 114 | } 115 | 116 | if len(stdout) == 0 { 117 | return "", errors.New("no staged changes found") 118 | } 119 | 120 | return string(stdout), nil 121 | } 122 | 123 | func commit(message string) error { 124 | cmd := exec.Command("git", "commit", "-m", message) 125 | output, err := cmd.CombinedOutput() 126 | if err != nil { 127 | return fmt.Errorf("error committing: %s", string(output)) 128 | } 129 | 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /apps/cli/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | 10 | openai "github.com/sashabaranov/go-openai" 11 | ) 12 | 13 | const ( 14 | MessageRoleSystem = "system" 15 | MessageRoleUser = "user" 16 | MessageRoleAssistant = "assistant" 17 | ) 18 | 19 | type MessageClient interface { 20 | CreateMessage(diff string, gitmojis []byte) (string, error) 21 | } 22 | 23 | // Ensure OpenAI satisfies the MessageClient interface 24 | var _ MessageClient = (*OpenAI)(nil) 25 | 26 | type OpenAI struct { 27 | apiKey string 28 | model Model 29 | } 30 | 31 | func NewOpenAI(apiKey string, model Model) *OpenAI { 32 | return &OpenAI{ 33 | apiKey, 34 | model, 35 | } 36 | } 37 | 38 | func (o *OpenAI) CreateMessage(diff string, gitmojis []byte) (string, error) { 39 | client := openai.NewClient(o.apiKey) 40 | resp, err := client.CreateChatCompletion( 41 | context.Background(), 42 | openai.ChatCompletionRequest{ 43 | Model: string(o.model), 44 | Messages: []openai.ChatCompletionMessage{ 45 | { 46 | Role: MessageRoleSystem, 47 | Content: SYSTEM_MESSAGE + string(gitmojis), 48 | }, 49 | { 50 | Role: MessageRoleUser, 51 | Content: prepareDiff(diff), 52 | }, 53 | }, 54 | }, 55 | ) 56 | 57 | if err != nil { 58 | return "", err 59 | } 60 | 61 | return resp.Choices[0].Message.Content, nil 62 | } 63 | 64 | type ClaudeMessage struct { 65 | Role string `json:"role"` 66 | Content string `json:"content"` 67 | } 68 | 69 | type ClaudeMessagesRequest struct { 70 | Model string `json:"model"` 71 | MaxTokens int `json:"max_tokens"` 72 | System string `json:"system"` 73 | Messages []ClaudeMessage `json:"messages"` 74 | } 75 | 76 | type ClaudeMessagesResponseContent struct { 77 | Text string `json:"text"` 78 | Type string `json:"type"` 79 | } 80 | 81 | type ClaudeMessagesResponseUsage struct { 82 | InputTokens int `json:"input_tokens"` 83 | OutputTokens int `json:"output_tokens"` 84 | } 85 | 86 | type ClaudeMessagesResponse struct { 87 | ID string `json:"id"` 88 | Role string `json:"role"` 89 | Model string `json:"model"` 90 | Content []ClaudeMessagesResponseContent `json:"content"` 91 | Usage ClaudeMessagesResponseUsage `json:"usage"` 92 | } 93 | 94 | // Ensure Anthropic satisfies the MessageClient interface 95 | var _ MessageClient = (*Anthropic)(nil) 96 | 97 | type Anthropic struct { 98 | apiKey string 99 | model Model 100 | } 101 | 102 | func NewAnthropic(apiKey string, model Model) *Anthropic { 103 | return &Anthropic{ 104 | apiKey, 105 | model, 106 | } 107 | } 108 | 109 | func (a *Anthropic) CreateMessage(diff string, gitmojis []byte) (string, error) { 110 | body, err := json.Marshal(map[string]interface{}{ 111 | "model": string(a.model), 112 | "max_tokens": 4096, 113 | "system": SYSTEM_MESSAGE + string(gitmojis), 114 | "messages": []ClaudeMessage{ 115 | { 116 | Role: MessageRoleUser, 117 | Content: prepareDiff(diff), 118 | }, 119 | }, 120 | }) 121 | 122 | if err != nil { 123 | return "", fmt.Errorf("error marshaling JSON payload: %v", err) 124 | } 125 | 126 | req, err := http.NewRequest(http.MethodPost, "https://api.anthropic.com/v1/messages", bytes.NewBuffer(body)) 127 | if err != nil { 128 | return "", fmt.Errorf("error creating request: %v", err) 129 | } 130 | 131 | req.Header.Set("Content-Type", "application/json") 132 | req.Header.Set("x-api-key", a.apiKey) 133 | req.Header.Set("anthropic-version", "2023-06-01") 134 | 135 | resp, err := http.DefaultClient.Do(req) 136 | if err != nil { 137 | return "", fmt.Errorf("error sending request: %v", err) 138 | } 139 | defer resp.Body.Close() 140 | 141 | if resp.StatusCode != http.StatusOK { 142 | return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) 143 | } 144 | 145 | var data ClaudeMessagesResponse 146 | if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { 147 | return "", fmt.Errorf("error decoding response: %v", err) 148 | } 149 | 150 | return data.Content[0].Text, nil 151 | } 152 | -------------------------------------------------------------------------------- /apps/web/app/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import * as React from 'react'; 3 | import { 4 | CaretSortIcon, 5 | CheckIcon, 6 | ChevronDownIcon, 7 | ChevronUpIcon, 8 | } from '@radix-ui/react-icons'; 9 | import * as SelectPrimitive from '@radix-ui/react-select'; 10 | 11 | import { cn } from 'lib/utils'; 12 | 13 | const Select = SelectPrimitive.Root; 14 | 15 | const SelectGroup = SelectPrimitive.Group; 16 | 17 | const SelectValue = SelectPrimitive.Value; 18 | 19 | const SelectTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, children, ...props }, ref) => ( 23 | span]:line-clamp-1', 27 | className 28 | )} 29 | {...props} 30 | > 31 | {children} 32 | 33 | 34 | 35 | 36 | )); 37 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 38 | 39 | const SelectScrollUpButton = React.forwardRef< 40 | React.ElementRef, 41 | React.ComponentPropsWithoutRef 42 | >(({ className, ...props }, ref) => ( 43 | 51 | 52 | 53 | )); 54 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 55 | 56 | const SelectScrollDownButton = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, ...props }, ref) => ( 60 | 68 | 69 | 70 | )); 71 | SelectScrollDownButton.displayName = 72 | SelectPrimitive.ScrollDownButton.displayName; 73 | 74 | const SelectContent = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef 77 | >(({ className, children, position = 'popper', ...props }, ref) => ( 78 | 79 | 90 | 91 | 98 | {children} 99 | 100 | 101 | 102 | 103 | )); 104 | SelectContent.displayName = SelectPrimitive.Content.displayName; 105 | 106 | const SelectLabel = React.forwardRef< 107 | React.ElementRef, 108 | React.ComponentPropsWithoutRef 109 | >(({ className, ...props }, ref) => ( 110 | 115 | )); 116 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 117 | 118 | const SelectItem = React.forwardRef< 119 | React.ElementRef, 120 | React.ComponentPropsWithoutRef 121 | >(({ className, children, ...props }, ref) => ( 122 | 130 | 131 | 132 | 133 | 134 | 135 | {children} 136 | 137 | )); 138 | SelectItem.displayName = SelectPrimitive.Item.displayName; 139 | 140 | const SelectSeparator = React.forwardRef< 141 | React.ElementRef, 142 | React.ComponentPropsWithoutRef 143 | >(({ className, ...props }, ref) => ( 144 | 149 | )); 150 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 151 | 152 | export { 153 | Select, 154 | SelectGroup, 155 | SelectValue, 156 | SelectTrigger, 157 | SelectContent, 158 | SelectLabel, 159 | SelectItem, 160 | SelectSeparator, 161 | SelectScrollUpButton, 162 | SelectScrollDownButton, 163 | }; 164 | -------------------------------------------------------------------------------- /apps/cli/go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= 6 | github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= 7 | github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 h1:6nVCV8pqGaeyxetur3gpX3AAaiyKgzjIoCPV3NXKZBE= 8 | github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= 9 | github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= 10 | github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= 11 | github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= 12 | github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= 13 | github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= 14 | github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= 15 | github.com/charmbracelet/huh/spinner v0.0.0-20240417163504-acfe24c3f5b5 h1:buNjWOts+d/CRtKxSpV7bupT3/yW24ArdORfbIL6Hg0= 16 | github.com/charmbracelet/huh/spinner v0.0.0-20240417163504-acfe24c3f5b5/go.mod h1:nrBG0YEHaxdbqHXW1xvG1hPqkuac9Eg7RTMvogiXuz0= 17 | github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= 18 | github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= 19 | github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= 20 | github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= 21 | github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= 22 | github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 23 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 24 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 25 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 26 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 27 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= 28 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= 29 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 30 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 31 | github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 32 | github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 33 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 34 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 35 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 36 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 37 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 38 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 42 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 43 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 44 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 45 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 46 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 47 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 48 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 49 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 50 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 51 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 52 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 53 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 54 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 55 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 56 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 57 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 58 | github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= 59 | github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 60 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 61 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 62 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 63 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 64 | github.com/sashabaranov/go-openai v1.24.0 h1:4H4Pg8Bl2RH/YSnU8DYumZbuHnnkfioor/dtNlB20D4= 65 | github.com/sashabaranov/go-openai v1.24.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 66 | github.com/segersniels/config v0.0.0-20240503115636-403023c44d9f h1:Hryp2S1kBLyBv78hV2/YQPzwz5xYy0VsixFBqYNXfHc= 67 | github.com/segersniels/config v0.0.0-20240503115636-403023c44d9f/go.mod h1:ZtYAvjzw4Y8B72nIjqWUnwYKeveEfiNbUilY/ts5MYE= 68 | github.com/segersniels/updater v1.1.1 h1:PwAxWaPONXyoYQohU7kWhp+yn8gotjBPpD6G2PyN0qo= 69 | github.com/segersniels/updater v1.1.1/go.mod h1:juYAsHmCiloHlG6YBrtRxoptQ3RZZswcktMtqQBQaBM= 70 | github.com/segersniels/updater v1.2.1 h1:wruRuD2clGyisb+DpBdlNa+v3tTN668xTiu1dygQe+k= 71 | github.com/segersniels/updater v1.2.1/go.mod h1:juYAsHmCiloHlG6YBrtRxoptQ3RZZswcktMtqQBQaBM= 72 | github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= 73 | github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 74 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 75 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 76 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 77 | golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 78 | golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= 79 | golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 80 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 81 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 82 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4= 83 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 84 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 85 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 87 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 89 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 90 | golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= 91 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 92 | golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= 93 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 94 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 95 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 96 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 97 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 98 | -------------------------------------------------------------------------------- /apps/web/resources/gitmojis.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://gitmoji.dev/api/gitmojis/schema", 3 | "gitmojis": [ 4 | { 5 | "emoji": "🎨", 6 | "entity": "🎨", 7 | "code": ":art:", 8 | "description": "Improve structure / format of the code.", 9 | "name": "art", 10 | "semver": null 11 | }, 12 | { 13 | "emoji": "⚡️", 14 | "entity": "⚡", 15 | "code": ":zap:", 16 | "description": "Improve performance.", 17 | "name": "zap", 18 | "semver": "patch" 19 | }, 20 | { 21 | "emoji": "🔥", 22 | "entity": "🔥", 23 | "code": ":fire:", 24 | "description": "Remove code or files.", 25 | "name": "fire", 26 | "semver": null 27 | }, 28 | { 29 | "emoji": "🐛", 30 | "entity": "🐛", 31 | "code": ":bug:", 32 | "description": "Fix a bug.", 33 | "name": "bug", 34 | "semver": "patch" 35 | }, 36 | { 37 | "emoji": "🚑️", 38 | "entity": "🚑", 39 | "code": ":ambulance:", 40 | "description": "Critical hotfix.", 41 | "name": "ambulance", 42 | "semver": "patch" 43 | }, 44 | { 45 | "emoji": "✨", 46 | "entity": "✨", 47 | "code": ":sparkles:", 48 | "description": "Introduce new features.", 49 | "name": "sparkles", 50 | "semver": "minor" 51 | }, 52 | { 53 | "emoji": "📝", 54 | "entity": "📝", 55 | "code": ":memo:", 56 | "description": "Add or update documentation.", 57 | "name": "memo", 58 | "semver": null 59 | }, 60 | { 61 | "emoji": "🚀", 62 | "entity": "🚀", 63 | "code": ":rocket:", 64 | "description": "Deploy stuff.", 65 | "name": "rocket", 66 | "semver": null 67 | }, 68 | { 69 | "emoji": "💄", 70 | "entity": "ff99cc;", 71 | "code": ":lipstick:", 72 | "description": "Add or update the UI and style files.", 73 | "name": "lipstick", 74 | "semver": "patch" 75 | }, 76 | { 77 | "emoji": "🎉", 78 | "entity": "🎉", 79 | "code": ":tada:", 80 | "description": "Begin a project.", 81 | "name": "tada", 82 | "semver": null 83 | }, 84 | { 85 | "emoji": "✅", 86 | "entity": "✅", 87 | "code": ":white_check_mark:", 88 | "description": "Add, update, or pass tests.", 89 | "name": "white-check-mark", 90 | "semver": null 91 | }, 92 | { 93 | "emoji": "🔒️", 94 | "entity": "🔒", 95 | "code": ":lock:", 96 | "description": "Fix security issues.", 97 | "name": "lock", 98 | "semver": "patch" 99 | }, 100 | { 101 | "emoji": "🔐", 102 | "entity": "🔐", 103 | "code": ":closed_lock_with_key:", 104 | "description": "Add or update secrets.", 105 | "name": "closed-lock-with-key", 106 | "semver": null 107 | }, 108 | { 109 | "emoji": "🔖", 110 | "entity": "🔖", 111 | "code": ":bookmark:", 112 | "description": "Release / Version tags.", 113 | "name": "bookmark", 114 | "semver": null 115 | }, 116 | { 117 | "emoji": "🚨", 118 | "entity": "🚨", 119 | "code": ":rotating_light:", 120 | "description": "Fix compiler / linter warnings.", 121 | "name": "rotating-light", 122 | "semver": null 123 | }, 124 | { 125 | "emoji": "🚧", 126 | "entity": "🚧", 127 | "code": ":construction:", 128 | "description": "Work in progress.", 129 | "name": "construction", 130 | "semver": null 131 | }, 132 | { 133 | "emoji": "💚", 134 | "entity": "💚", 135 | "code": ":green_heart:", 136 | "description": "Fix CI Build.", 137 | "name": "green-heart", 138 | "semver": null 139 | }, 140 | { 141 | "emoji": "⬇️", 142 | "entity": "⬇️", 143 | "code": ":arrow_down:", 144 | "description": "Downgrade dependencies.", 145 | "name": "arrow-down", 146 | "semver": "patch" 147 | }, 148 | { 149 | "emoji": "⬆️", 150 | "entity": "⬆️", 151 | "code": ":arrow_up:", 152 | "description": "Upgrade dependencies.", 153 | "name": "arrow-up", 154 | "semver": "patch" 155 | }, 156 | { 157 | "emoji": "📌", 158 | "entity": "📌", 159 | "code": ":pushpin:", 160 | "description": "Pin dependencies to specific versions.", 161 | "name": "pushpin", 162 | "semver": "patch" 163 | }, 164 | { 165 | "emoji": "👷", 166 | "entity": "👷", 167 | "code": ":construction_worker:", 168 | "description": "Add or update CI build system.", 169 | "name": "construction-worker", 170 | "semver": null 171 | }, 172 | { 173 | "emoji": "📈", 174 | "entity": "📈", 175 | "code": ":chart_with_upwards_trend:", 176 | "description": "Add or update analytics or track code.", 177 | "name": "chart-with-upwards-trend", 178 | "semver": "patch" 179 | }, 180 | { 181 | "emoji": "♻️", 182 | "entity": "♻", 183 | "code": ":recycle:", 184 | "description": "Refactor code.", 185 | "name": "recycle", 186 | "semver": null 187 | }, 188 | { 189 | "emoji": "➕", 190 | "entity": "➕", 191 | "code": ":heavy_plus_sign:", 192 | "description": "Add a dependency.", 193 | "name": "heavy-plus-sign", 194 | "semver": "patch" 195 | }, 196 | { 197 | "emoji": "➖", 198 | "entity": "➖", 199 | "code": ":heavy_minus_sign:", 200 | "description": "Remove a dependency.", 201 | "name": "heavy-minus-sign", 202 | "semver": "patch" 203 | }, 204 | { 205 | "emoji": "🔧", 206 | "entity": "🔧", 207 | "code": ":wrench:", 208 | "description": "Add or update configuration files.", 209 | "name": "wrench", 210 | "semver": "patch" 211 | }, 212 | { 213 | "emoji": "🔨", 214 | "entity": "🔨", 215 | "code": ":hammer:", 216 | "description": "Add or update development scripts.", 217 | "name": "hammer", 218 | "semver": null 219 | }, 220 | { 221 | "emoji": "🌐", 222 | "entity": "🌐", 223 | "code": ":globe_with_meridians:", 224 | "description": "Internationalization and localization.", 225 | "name": "globe-with-meridians", 226 | "semver": "patch" 227 | }, 228 | { 229 | "emoji": "✏️", 230 | "entity": "", 231 | "code": ":pencil2:", 232 | "description": "Fix typos.", 233 | "name": "pencil2", 234 | "semver": "patch" 235 | }, 236 | { 237 | "emoji": "💩", 238 | "entity": "", 239 | "code": ":poop:", 240 | "description": "Write bad code that needs to be improved.", 241 | "name": "poop", 242 | "semver": null 243 | }, 244 | { 245 | "emoji": "⏪️", 246 | "entity": "⏪", 247 | "code": ":rewind:", 248 | "description": "Revert changes.", 249 | "name": "rewind", 250 | "semver": "patch" 251 | }, 252 | { 253 | "emoji": "🔀", 254 | "entity": "🔀", 255 | "code": ":twisted_rightwards_arrows:", 256 | "description": "Merge branches.", 257 | "name": "twisted-rightwards-arrows", 258 | "semver": null 259 | }, 260 | { 261 | "emoji": "📦️", 262 | "entity": "F4E6;", 263 | "code": ":package:", 264 | "description": "Add or update compiled files or packages.", 265 | "name": "package", 266 | "semver": "patch" 267 | }, 268 | { 269 | "emoji": "👽️", 270 | "entity": "F47D;", 271 | "code": ":alien:", 272 | "description": "Update code due to external API changes.", 273 | "name": "alien", 274 | "semver": "patch" 275 | }, 276 | { 277 | "emoji": "🚚", 278 | "entity": "F69A;", 279 | "code": ":truck:", 280 | "description": "Move or rename resources (e.g.: files, paths, routes).", 281 | "name": "truck", 282 | "semver": null 283 | }, 284 | { 285 | "emoji": "📄", 286 | "entity": "F4C4;", 287 | "code": ":page_facing_up:", 288 | "description": "Add or update license.", 289 | "name": "page-facing-up", 290 | "semver": null 291 | }, 292 | { 293 | "emoji": "💥", 294 | "entity": "💥", 295 | "code": ":boom:", 296 | "description": "Introduce breaking changes.", 297 | "name": "boom", 298 | "semver": "major" 299 | }, 300 | { 301 | "emoji": "🍱", 302 | "entity": "F371", 303 | "code": ":bento:", 304 | "description": "Add or update assets.", 305 | "name": "bento", 306 | "semver": "patch" 307 | }, 308 | { 309 | "emoji": "♿️", 310 | "entity": "♿", 311 | "code": ":wheelchair:", 312 | "description": "Improve accessibility.", 313 | "name": "wheelchair", 314 | "semver": "patch" 315 | }, 316 | { 317 | "emoji": "💡", 318 | "entity": "💡", 319 | "code": ":bulb:", 320 | "description": "Add or update comments in source code.", 321 | "name": "bulb", 322 | "semver": null 323 | }, 324 | { 325 | "emoji": "🍻", 326 | "entity": "🍻", 327 | "code": ":beers:", 328 | "description": "Write code drunkenly.", 329 | "name": "beers", 330 | "semver": null 331 | }, 332 | { 333 | "emoji": "💬", 334 | "entity": "💬", 335 | "code": ":speech_balloon:", 336 | "description": "Add or update text and literals.", 337 | "name": "speech-balloon", 338 | "semver": "patch" 339 | }, 340 | { 341 | "emoji": "🗃️", 342 | "entity": "🗃", 343 | "code": ":card_file_box:", 344 | "description": "Perform database related changes.", 345 | "name": "card-file-box", 346 | "semver": "patch" 347 | }, 348 | { 349 | "emoji": "🔊", 350 | "entity": "🔊", 351 | "code": ":loud_sound:", 352 | "description": "Add or update logs.", 353 | "name": "loud-sound", 354 | "semver": null 355 | }, 356 | { 357 | "emoji": "🔇", 358 | "entity": "🔇", 359 | "code": ":mute:", 360 | "description": "Remove logs.", 361 | "name": "mute", 362 | "semver": null 363 | }, 364 | { 365 | "emoji": "👥", 366 | "entity": "👥", 367 | "code": ":busts_in_silhouette:", 368 | "description": "Add or update contributor(s).", 369 | "name": "busts-in-silhouette", 370 | "semver": null 371 | }, 372 | { 373 | "emoji": "🚸", 374 | "entity": "🚸", 375 | "code": ":children_crossing:", 376 | "description": "Improve user experience / usability.", 377 | "name": "children-crossing", 378 | "semver": "patch" 379 | }, 380 | { 381 | "emoji": "🏗️", 382 | "entity": "f3d7;", 383 | "code": ":building_construction:", 384 | "description": "Make architectural changes.", 385 | "name": "building-construction", 386 | "semver": null 387 | }, 388 | { 389 | "emoji": "📱", 390 | "entity": "📱", 391 | "code": ":iphone:", 392 | "description": "Work on responsive design.", 393 | "name": "iphone", 394 | "semver": "patch" 395 | }, 396 | { 397 | "emoji": "🤡", 398 | "entity": "🤡", 399 | "code": ":clown_face:", 400 | "description": "Mock things.", 401 | "name": "clown-face", 402 | "semver": null 403 | }, 404 | { 405 | "emoji": "🥚", 406 | "entity": "🥚", 407 | "code": ":egg:", 408 | "description": "Add or update an easter egg.", 409 | "name": "egg", 410 | "semver": "patch" 411 | }, 412 | { 413 | "emoji": "🙈", 414 | "entity": "bdfe7;", 415 | "code": ":see_no_evil:", 416 | "description": "Add or update a .gitignore file.", 417 | "name": "see-no-evil", 418 | "semver": null 419 | }, 420 | { 421 | "emoji": "📸", 422 | "entity": "📸", 423 | "code": ":camera_flash:", 424 | "description": "Add or update snapshots.", 425 | "name": "camera-flash", 426 | "semver": null 427 | }, 428 | { 429 | "emoji": "⚗️", 430 | "entity": "📸", 431 | "code": ":alembic:", 432 | "description": "Perform experiments.", 433 | "name": "alembic", 434 | "semver": "patch" 435 | }, 436 | { 437 | "emoji": "🔍️", 438 | "entity": "🔍", 439 | "code": ":mag:", 440 | "description": "Improve SEO.", 441 | "name": "mag", 442 | "semver": "patch" 443 | }, 444 | { 445 | "emoji": "🏷️", 446 | "entity": "🏷", 447 | "code": ":label:", 448 | "description": "Add or update types.", 449 | "name": "label", 450 | "semver": "patch" 451 | }, 452 | { 453 | "emoji": "🌱", 454 | "entity": "🌱", 455 | "code": ":seedling:", 456 | "description": "Add or update seed files.", 457 | "name": "seedling", 458 | "semver": null 459 | }, 460 | { 461 | "emoji": "🚩", 462 | "entity": "🚩", 463 | "code": ":triangular_flag_on_post:", 464 | "description": "Add, update, or remove feature flags.", 465 | "name": "triangular-flag-on-post", 466 | "semver": "patch" 467 | }, 468 | { 469 | "emoji": "🥅", 470 | "entity": "🥅", 471 | "code": ":goal_net:", 472 | "description": "Catch errors.", 473 | "name": "goal-net", 474 | "semver": "patch" 475 | }, 476 | { 477 | "emoji": "💫", 478 | "entity": "💫", 479 | "code": ":dizzy:", 480 | "description": "Add or update animations and transitions.", 481 | "name": "animation", 482 | "semver": "patch" 483 | }, 484 | { 485 | "emoji": "🗑️", 486 | "entity": "🗑", 487 | "code": ":wastebasket:", 488 | "description": "Deprecate code that needs to be cleaned up.", 489 | "name": "wastebasket", 490 | "semver": "patch" 491 | }, 492 | { 493 | "emoji": "🛂", 494 | "entity": "🛂", 495 | "code": ":passport_control:", 496 | "description": "Work on code related to authorization, roles and permissions.", 497 | "name": "passport-control", 498 | "semver": "patch" 499 | }, 500 | { 501 | "emoji": "🩹", 502 | "entity": "🩹", 503 | "code": ":adhesive_bandage:", 504 | "description": "Simple fix for a non-critical issue.", 505 | "name": "adhesive-bandage", 506 | "semver": "patch" 507 | }, 508 | { 509 | "emoji": "🧐", 510 | "entity": "🧐", 511 | "code": ":monocle_face:", 512 | "description": "Data exploration/inspection.", 513 | "name": "monocle-face", 514 | "semver": null 515 | }, 516 | { 517 | "emoji": "⚰️", 518 | "entity": "⚰", 519 | "code": ":coffin:", 520 | "description": "Remove dead code.", 521 | "name": "coffin", 522 | "semver": null 523 | }, 524 | { 525 | "emoji": "🧪", 526 | "entity": "🧪", 527 | "code": ":test_tube:", 528 | "description": "Add a failing test.", 529 | "name": "test-tube", 530 | "semver": null 531 | }, 532 | { 533 | "emoji": "👔", 534 | "entity": "👔", 535 | "code": ":necktie:", 536 | "description": "Add or update business logic.", 537 | "name": "necktie", 538 | "semver": "patch" 539 | }, 540 | { 541 | "emoji": "🩺", 542 | "entity": "🩺", 543 | "code": ":stethoscope:", 544 | "description": "Add or update healthcheck.", 545 | "name": "stethoscope", 546 | "semver": null 547 | }, 548 | { 549 | "emoji": "🧱", 550 | "entity": "🧱", 551 | "code": ":bricks:", 552 | "description": "Infrastructure related changes.", 553 | "name": "bricks", 554 | "semver": null 555 | }, 556 | { 557 | "emoji": "🧑💻", 558 | "entity": "🧑💻", 559 | "code": ":technologist:", 560 | "description": "Improve developer experience.", 561 | "name": "technologist", 562 | "semver": null 563 | }, 564 | { 565 | "emoji": "💸", 566 | "entity": "💸", 567 | "code": ":money_with_wings:", 568 | "description": "Add sponsorships or money related infrastructure.", 569 | "name": "money-with-wings", 570 | "semver": null 571 | }, 572 | { 573 | "emoji": "🧵", 574 | "entity": "🧵", 575 | "code": ":thread:", 576 | "description": "Add or update code related to multithreading or concurrency.", 577 | "name": "thread", 578 | "semver": null 579 | }, 580 | { 581 | "emoji": "🦺", 582 | "entity": "🦺", 583 | "code": ":safety_vest:", 584 | "description": "Add or update code related to validation.", 585 | "name": "safety-vest", 586 | "semver": null 587 | } 588 | ] 589 | } 590 | --------------------------------------------------------------------------------
15 | Why spend time thinking about commit messages when you can generate 16 | them using AI? 17 |
86 | {JSON.stringify(error, null, 2)} 87 |