feat: initial project setup with gai tool

This commit is contained in:
2026-06-09 16:41:02 +08:00
commit a058881b53
17 changed files with 952 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# gai configuration
GAI_API_KEY=sk-your-api-key-here
GAI_API_BASE=https://api.openai.com/v1
GAI_MODEL=gpt-4o
GAI_MAX_TOKENS=500
GAI_TEMPERATURE=0.7
+34
View File
@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
+106
View File
@@ -0,0 +1,106 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
import { createRoot } from "react-dom/client";
// import .css files directly and it works
import './index.css';
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
+15
View File
@@ -0,0 +1,15 @@
# git-ai
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+26
View File
@@ -0,0 +1,26 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "git-ai",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.14", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.14.tgz", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/node": ["@types/node@25.9.2", "https://registry.npmmirror.com/@types/node/-/node-25.9.2.tgz", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
"bun-types": ["bun-types@1.3.14", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.24.6", "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
}
}
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env bun
import * as readline from "node:readline";
import { loadConfig, saveConfig } from "./src/config";
import {
isGitRepo,
getRepoRoot,
getStagedFiles,
getUnstagedFiles,
getStagedDiff,
getRecentCommits,
stageFiles,
commit,
} from "./src/git";
import { selectFiles } from "./src/selector";
import { collectProjectContext } from "./src/context";
import { buildPrompt, SYSTEM_PROMPT } from "./src/prompt";
import { generateCommitMessage } from "./src/ai";
import { copyToClipboard } from "./src/clipboard";
import { BOLD, GREEN, YELLOW, CYAN, RED, DIM, RESET } from "./src/terminal";
import type { Config } from "./src/types";
const args = process.argv.slice(2);
function showHelp() {
console.log(`
${BOLD}gai${RESET} — AI-powered git commit message generator
${BOLD}Usage:${RESET}
gai Generate commit message for staged/changed files
gai --auto Auto-stage all changed files
gai --dry-run Generate message without committing
gai config Configure API settings
gai --help Show this help message
gai --version Show version
${BOLD}Configuration:${RESET}
Set via ${CYAN}gai config${RESET} or environment variables:
GAI_API_KEY OpenAI-compatible API key
GAI_API_BASE API base URL (default: https://api.openai.com/v1)
GAI_MODEL Model name (default: gpt-4o)
GAI_MAX_TOKENS Max tokens (default: 500)
GAI_TEMPERATURE Temperature (default: 0.7)
`);
}
function ask(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
async function handleConfig() {
const config = await loadConfig();
console.log(`\n ${BOLD}Current configuration:${RESET}`);
console.log(
` API Key: ${config.apiKey ? "****" + config.apiKey.slice(-4) : `${YELLOW}(not set)${RESET}`}`,
);
console.log(` API Base: ${config.apiBase}`);
console.log(` Model: ${config.model}`);
console.log(` Max Tokens: ${config.maxTokens}`);
console.log(` Temperature: ${config.temperature}`);
console.log(
`\n ${BOLD}Enter new values (leave empty to keep current):${RESET}`,
);
const apiKey = await ask(" API Key: ");
const apiBase = await ask(` API Base [${config.apiBase}]: `);
const model = await ask(` Model [${config.model}]: `);
const maxTokens = await ask(` Max Tokens [${config.maxTokens}]: `);
const temperature = await ask(` Temperature [${config.temperature}]: `);
const updates: Partial<Config> = {};
if (apiKey) updates.apiKey = apiKey;
if (apiBase) updates.apiBase = apiBase;
if (model) updates.model = model;
if (maxTokens) updates.maxTokens = parseInt(maxTokens);
if (temperature) updates.temperature = parseFloat(temperature);
if (Object.keys(updates).length > 0) {
await saveConfig(updates);
console.log(`\n ${GREEN}Configuration saved!${RESET}`);
} else {
console.log("\n No changes.");
}
}
async function confirmCommit(message: string): Promise<"y" | "n" | "e"> {
console.log(`\n ${BOLD}Generated commit message:${RESET}`);
console.log(` ${GREEN}${message}${RESET}\n`);
const answer = await ask(` Use this message? [${GREEN}Y${RESET}/n/e] `);
const lower = answer.toLowerCase();
if (lower === "n") return "n";
if (lower === "e") return "e";
return "y";
}
async function editMessage(current: string): Promise<string | null> {
console.log(` Current: ${DIM}${current}${RESET}`);
console.log(` Enter new message (empty to abort):`);
const newMsg = await ask(" > ");
return newMsg || null;
}
async function main() {
if (args.includes("--help") || args.includes("-h")) {
showHelp();
return;
}
if (args.includes("--version") || args.includes("-v")) {
console.log("gai v0.1.0");
return;
}
if (args[0] === "config") {
await handleConfig();
return;
}
const autoMode = args.includes("--auto") || args.includes("-a");
const dryRun = args.includes("--dry-run") || args.includes("-d");
const config = await loadConfig();
if (!config.apiKey) {
console.error(
` ${RED}Error: API key not set. Run ${BOLD}gai config${RESET}${RED} to configure.${RESET}`,
);
process.exit(1);
}
if (!(await isGitRepo())) {
console.error(` ${RED}Error: Not a git repository.${RESET}`);
process.exit(1);
}
const stagedFiles = await getStagedFiles();
const unstagedFiles = await getUnstagedFiles();
if (stagedFiles.length === 0 && unstagedFiles.length === 0) {
console.log(" Nothing to commit.");
return;
}
if (unstagedFiles.length > 0) {
if (autoMode) {
await stageFiles(unstagedFiles.map((f) => f.path));
console.log(
` ${GREEN}Auto-staged ${unstagedFiles.length} file(s).${RESET}`,
);
} else {
const selected = await selectFiles(stagedFiles, unstagedFiles);
if (selected.length > 0) {
await stageFiles(selected);
console.log(
` ${GREEN}Staged ${selected.length} file(s).${RESET}`,
);
}
}
}
const diff = await getStagedDiff();
if (!diff) {
console.log(" No staged changes to commit.");
return;
}
const MAX_DIFF_SIZE = 15000;
const truncatedDiff =
diff.length > MAX_DIFF_SIZE
? diff.substring(0, MAX_DIFF_SIZE) + "\n... (truncated)"
: diff;
const repoRoot = await getRepoRoot();
const projectCtx = await collectProjectContext(repoRoot);
const recentCommits = await getRecentCommits(10);
const userPrompt = buildPrompt({
readme: projectCtx.readme,
packageDescription: projectCtx.packageDescription,
structure: projectCtx.structure,
recentCommits,
diff: truncatedDiff,
});
console.log("\n Generating commit message...");
let message: string;
try {
message = await generateCommitMessage(config, SYSTEM_PROMPT, userPrompt);
} catch (err) {
console.error(
` ${RED}AI request failed: ${err instanceof Error ? err.message : err}${RESET}`,
);
process.exit(1);
}
if (dryRun) {
console.log(`\n ${BOLD}Generated commit message:${RESET}`);
console.log(` ${GREEN}${message}${RESET}`);
await copyToClipboard(message);
console.log(`\n ${DIM}(dry-run, message copied to clipboard)${RESET}`);
return;
}
const action = await confirmCommit(message);
if (action === "y") {
try {
await commit(message);
console.log(`\n ${GREEN}Committed successfully!${RESET}`);
} catch (err) {
console.error(
` ${RED}Commit failed: ${err instanceof Error ? err.message : err}${RESET}`,
);
process.exit(1);
}
} else if (action === "e") {
const edited = await editMessage(message);
if (edited) {
try {
await commit(edited);
console.log(`\n ${GREEN}Committed successfully!${RESET}`);
} catch (err) {
console.error(
` ${RED}Commit failed: ${err instanceof Error ? err.message : err}${RESET}`,
);
process.exit(1);
}
} else {
console.log(" Aborted.");
}
} else {
const copied = await copyToClipboard(message);
console.log(
copied
? ` Aborted. Message copied to clipboard.`
: ` Aborted. Message: ${message}`,
);
}
}
main().catch((err) => {
console.error(` ${RED}Unexpected error: ${err}${RESET}`);
process.exit(1);
});
+21
View File
@@ -0,0 +1,21 @@
{
"name": "git-ai",
"version": "0.1.0",
"description": "AI-powered git commit message generator",
"module": "index.ts",
"type": "module",
"bin": {
"gai": "./index.ts"
},
"scripts": {
"gai": "bun run index.ts",
"build": "bun build --compile index.ts --outfile gai"
},
"private": false,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}
+55
View File
@@ -0,0 +1,55 @@
import type { Config } from "./types";
interface ChatMessage {
role: "system" | "user" | "assistant";
content: string;
}
interface ChatCompletionResponse {
choices?: Array<{
message?: {
content?: string;
};
}>;
}
export async function generateCommitMessage(
config: Config,
systemPrompt: string,
userPrompt: string,
): Promise<string> {
const url = `${config.apiBase.replace(/\/$/, "")}/chat/completions`;
const messages: ChatMessage[] = [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
];
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model,
max_tokens: config.maxTokens,
temperature: config.temperature,
messages,
}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`API request failed (${response.status}): ${text}`);
}
const data = (await response.json()) as ChatCompletionResponse;
const message = data.choices?.[0]?.message?.content?.trim();
if (!message) {
throw new Error("Empty response from AI");
}
return message;
}
+26
View File
@@ -0,0 +1,26 @@
export async function copyToClipboard(text: string): Promise<boolean> {
const commands: string[][] = [];
if (process.platform === "darwin") {
commands.push(["pbcopy"]);
} else if (process.platform === "linux") {
commands.push(["xclip", "-selection", "clipboard"]);
commands.push(["xsel", "--clipboard", "--input"]);
}
for (const cmd of commands) {
try {
const proc = Bun.spawn(cmd, {
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
});
proc.stdin.write(text);
proc.stdin.end();
const exitCode = await proc.exited;
if (exitCode === 0) return true;
} catch {}
}
return false;
}
+48
View File
@@ -0,0 +1,48 @@
import { existsSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import type { Config } from "./types";
const DEFAULT_CONFIG: Config = {
apiKey: "",
apiBase: "https://api.openai.com/v1",
model: "gpt-4o",
maxTokens: 500,
temperature: 0.7,
};
const CONFIG_DIR = join(homedir(), ".config", "gai");
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
export async function loadConfig(): Promise<Config> {
let config = { ...DEFAULT_CONFIG };
if (existsSync(CONFIG_PATH)) {
try {
const file = Bun.file(CONFIG_PATH);
const json = (await file.json()) as Partial<Config>;
config = { ...config, ...json };
} catch {}
}
if (process.env.GAI_API_KEY) config.apiKey = process.env.GAI_API_KEY;
if (process.env.GAI_API_BASE) config.apiBase = process.env.GAI_API_BASE;
if (process.env.GAI_MODEL) config.model = process.env.GAI_MODEL;
if (process.env.GAI_MAX_TOKENS)
config.maxTokens = parseInt(process.env.GAI_MAX_TOKENS);
if (process.env.GAI_TEMPERATURE)
config.temperature = parseFloat(process.env.GAI_TEMPERATURE);
return config;
}
export async function saveConfig(partial: Partial<Config>): Promise<void> {
if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { recursive: true });
}
const current = await loadConfig();
const merged = { ...current, ...partial };
await Bun.write(CONFIG_PATH, JSON.stringify(merged, null, 2));
}
+73
View File
@@ -0,0 +1,73 @@
import { existsSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
const IGNORED_DIRS = new Set([
"node_modules",
".git",
"dist",
"out",
".next",
"__pycache__",
".cache",
".turbo",
"coverage",
".vscode",
".idea",
"target",
"build",
".gradle",
"vendor",
]);
export async function collectProjectContext(repoRoot: string): Promise<{
readme: string | null;
packageDescription: string | null;
structure: string | null;
}> {
let readme: string | null = null;
let packageDescription: string | null = null;
let structure: string | null = null;
for (const name of ["README.md", "README.txt", "README.rst", "README"]) {
const filePath = join(repoRoot, name);
if (existsSync(filePath)) {
try {
const content = await Bun.file(filePath).text();
readme =
content.length > 1500
? content.substring(0, 1500) + "\n... (truncated)"
: content;
} catch {}
break;
}
}
const pkgPath = join(repoRoot, "package.json");
if (existsSync(pkgPath)) {
try {
const pkg = (await Bun.file(pkgPath).json()) as { description?: string };
if (pkg.description) {
packageDescription = pkg.description;
}
} catch {}
}
try {
const entries = readdirSync(repoRoot)
.filter((name) => !IGNORED_DIRS.has(name) && !name.startsWith("."))
.map((name) => {
try {
return statSync(join(repoRoot, name)).isDirectory()
? `${name}/`
: name;
} catch {
return name;
}
});
if (entries.length > 0) {
structure = entries.join(", ");
}
} catch {}
return { readme, packageDescription, structure };
}
+110
View File
@@ -0,0 +1,110 @@
import type { FileEntry } from "./types";
export async function isGitRepo(): Promise<boolean> {
try {
await Bun.$`git rev-parse --is-inside-work-tree`.quiet();
return true;
} catch {
return false;
}
}
export async function getRepoRoot(): Promise<string> {
const result = await Bun.$`git rev-parse --show-toplevel`.quiet().text();
return result.trim();
}
function statusToLabel(status: string): string {
switch (status[0]) {
case "A":
return "new";
case "D":
return "deleted";
case "R":
return "renamed";
case "C":
return "copied";
case "M":
return "modified";
case "T":
return "type change";
default:
return "modified";
}
}
function parseNameStatus(output: string): FileEntry[] {
return output
.trim()
.split("\n")
.filter(Boolean)
.map((line) => {
const [status, ...pathParts] = line.split("\t");
const path = pathParts[pathParts.length - 1] ?? "";
return { path, status: status!, label: statusToLabel(status!) };
});
}
export async function getStagedFiles(): Promise<FileEntry[]> {
try {
const result =
await Bun.$`git diff --cached --name-status`.quiet().text();
return parseNameStatus(result);
} catch {
return [];
}
}
export async function getUnstagedFiles(): Promise<FileEntry[]> {
const files: FileEntry[] = [];
try {
const result = await Bun.$`git diff --name-status`.quiet().text();
files.push(...parseNameStatus(result));
} catch {}
try {
const result =
await Bun.$`git ls-files --others --exclude-standard`.quiet().text();
for (const path of result.trim().split("\n").filter(Boolean)) {
files.push({ path, status: "??", label: "new" });
}
} catch {}
return files;
}
export async function getStagedDiff(): Promise<string> {
try {
const result = await Bun.$`git diff --staged`.quiet().text();
return result.trim();
} catch {
return "";
}
}
export async function getRecentCommits(count = 10): Promise<string[]> {
try {
const n = String(count);
const result = await Bun.$`git log --oneline -n ${n}`.quiet().text();
return result.trim().split("\n").filter(Boolean);
} catch {
return [];
}
}
export async function stageFiles(paths: string[]): Promise<void> {
if (paths.length === 0) return;
await Bun.$`git add -- ${paths}`;
}
export async function commit(message: string): Promise<void> {
const proc = Bun.spawn(["git", "commit", "-m", message], {
stdout: "inherit",
stderr: "inherit",
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(`git commit failed (exit code ${exitCode})`);
}
}
+56
View File
@@ -0,0 +1,56 @@
import type { ProjectContext } from "./types";
export const SYSTEM_PROMPT = `You are an expert at writing concise, meaningful git commit messages following the Conventional Commits specification.
Format: <type>(<scope>): <description>
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
Rules:
1. Use imperative mood in description (e.g., "add feature" not "added feature")
2. Keep the first line (subject) under 72 characters
3. Scope is optional but recommended when the change area is clear
4. For breaking changes, add "!" after the scope: feat(scope)!: description
5. If the change needs explanation, add a body separated by a blank line — explain WHY, not WHAT
6. Match the language and style of recent commits if provided
7. Be specific — avoid vague messages like "update code" or "fix bugs"
8. Output ONLY the commit message text, no markdown, no code blocks, no prefixes`;
export function buildPrompt(context: ProjectContext): string {
const parts: string[] = [];
if (
context.packageDescription ||
context.readme ||
context.structure
) {
parts.push("## Project Context");
if (context.packageDescription) {
parts.push(`Description: ${context.packageDescription}`);
}
if (context.structure) {
parts.push(`Structure: ${context.structure}`);
}
if (context.readme) {
parts.push(`README:\n${context.readme}`);
}
parts.push("");
}
if (context.recentCommits.length > 0) {
parts.push("## Recent Commits (for style reference)");
for (const c of context.recentCommits) {
parts.push(c);
}
parts.push("");
}
parts.push("## Staged Changes");
parts.push("```diff");
parts.push(context.diff);
parts.push("```");
parts.push("");
parts.push("Generate a commit message for the above changes.");
return parts.join("\n");
}
+62
View File
@@ -0,0 +1,62 @@
import * as readline from "node:readline";
import type { FileEntry } from "./types";
import { BOLD, GREEN, YELLOW, CYAN, RESET } from "./terminal";
export async function selectFiles(
stagedFiles: FileEntry[],
unstagedFiles: FileEntry[],
): Promise<string[]> {
if (unstagedFiles.length === 0) return [];
if (stagedFiles.length > 0) {
console.log(`\n ${BOLD}Staged files (will be included):${RESET}`);
for (const f of stagedFiles) {
console.log(` ${GREEN}${RESET} ${f.path} (${YELLOW}${f.label}${RESET})`);
}
}
console.log(`\n ${BOLD}Unstaged files:${RESET}`);
for (let i = 0; i < unstagedFiles.length; i++) {
const f = unstagedFiles[i]!;
console.log(
` ${CYAN}${i + 1}.${RESET} ${f.path} (${YELLOW}${f.label}${RESET})`,
);
}
console.log("");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(
" Enter files to stage (e.g. 1,3) or 'a' for all: ",
(answer) => {
rl.close();
const trimmed = answer.trim().toLowerCase();
if (trimmed === "a" || trimmed === "all") {
resolve(unstagedFiles.map((f) => f.path));
return;
}
if (trimmed === "") {
resolve([]);
return;
}
const indices = trimmed
.split(/[,\s]+/)
.map((s) => parseInt(s.trim()))
.filter(
(n) => !isNaN(n) && n >= 1 && n <= unstagedFiles.length,
)
.map((n) => n - 1);
const uniqueIndices = [...new Set(indices)];
resolve(uniqueIndices.map((i) => unstagedFiles[i]!.path));
},
);
});
}
+7
View File
@@ -0,0 +1,7 @@
export const BOLD = "\x1b[1m";
export const GREEN = "\x1b[32m";
export const YELLOW = "\x1b[33m";
export const CYAN = "\x1b[36m";
export const RED = "\x1b[31m";
export const DIM = "\x1b[2m";
export const RESET = "\x1b[0m";
+21
View File
@@ -0,0 +1,21 @@
export interface Config {
apiKey: string;
apiBase: string;
model: string;
maxTokens: number;
temperature: number;
}
export interface FileEntry {
path: string;
status: string;
label: string;
}
export interface ProjectContext {
readme: string | null;
packageDescription: string | null;
structure: string | null;
recentCommits: string[];
diff: string;
}
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}