diff --git a/.gitignore b/.gitignore index 89df803..10e6c59 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ pnpm-debug.log* # environment variables .env .env.production +src/utils/.env # macOS-specific files .DS_Store diff --git a/package.json b/package.json index ed96874..825b20e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "convert:avif": "node src/utils/convert-to-avif.js", "convert:avif:all": "node src/utils/convert-to-avif.js --all", "convert:avif:jpeg": "node src/utils/convert-to-avif.js --jpeg", - "convert:avif:png": "node src/utils/convert-to-avif.js --png" + "convert:avif:png": "node src/utils/convert-to-avif.js --png", + "commit": "node src/utils/git-commit.js" }, "dependencies": { "@astrojs/cloudflare": "^12.6.12", diff --git a/src/assets/blog-placeholder-1.avif b/src/assets/blog-placeholder-1.avif deleted file mode 100644 index 9480c0e..0000000 Binary files a/src/assets/blog-placeholder-1.avif and /dev/null differ diff --git a/src/assets/blog-placeholder-1.jpg b/src/assets/blog-placeholder-1.jpg deleted file mode 100644 index 74d4009..0000000 Binary files a/src/assets/blog-placeholder-1.jpg and /dev/null differ diff --git a/src/assets/blog-placeholder-2.avif b/src/assets/blog-placeholder-2.avif deleted file mode 100644 index 73f3fc9..0000000 Binary files a/src/assets/blog-placeholder-2.avif and /dev/null differ diff --git a/src/assets/blog-placeholder-2.jpg b/src/assets/blog-placeholder-2.jpg deleted file mode 100644 index c4214b0..0000000 Binary files a/src/assets/blog-placeholder-2.jpg and /dev/null differ diff --git a/src/assets/blog-placeholder-3.avif b/src/assets/blog-placeholder-3.avif deleted file mode 100644 index 5ff93a6..0000000 Binary files a/src/assets/blog-placeholder-3.avif and /dev/null differ diff --git a/src/assets/blog-placeholder-3.jpg b/src/assets/blog-placeholder-3.jpg deleted file mode 100644 index fbe2ac0..0000000 Binary files a/src/assets/blog-placeholder-3.jpg and /dev/null differ diff --git a/src/assets/blog-placeholder-4.avif b/src/assets/blog-placeholder-4.avif deleted file mode 100644 index 8059858..0000000 Binary files a/src/assets/blog-placeholder-4.avif and /dev/null differ diff --git a/src/assets/blog-placeholder-4.jpg b/src/assets/blog-placeholder-4.jpg deleted file mode 100644 index f4fc88e..0000000 Binary files a/src/assets/blog-placeholder-4.jpg and /dev/null differ diff --git a/src/assets/blog-placeholder-5.avif b/src/assets/blog-placeholder-5.avif deleted file mode 100644 index a1ed108..0000000 Binary files a/src/assets/blog-placeholder-5.avif and /dev/null differ diff --git a/src/assets/blog-placeholder-5.jpg b/src/assets/blog-placeholder-5.jpg deleted file mode 100644 index c564674..0000000 Binary files a/src/assets/blog-placeholder-5.jpg and /dev/null differ diff --git a/src/assets/blog-placeholder-about.avif b/src/assets/blog-placeholder-about.avif deleted file mode 100644 index a8d119b..0000000 Binary files a/src/assets/blog-placeholder-about.avif and /dev/null differ diff --git a/src/assets/blog-placeholder-about.jpg b/src/assets/blog-placeholder-about.jpg deleted file mode 100644 index cf5f685..0000000 Binary files a/src/assets/blog-placeholder-about.jpg and /dev/null differ diff --git a/src/assets/claude-nuke.png b/src/assets/claude-nuke.png deleted file mode 100644 index 4d8913e..0000000 Binary files a/src/assets/claude-nuke.png and /dev/null differ diff --git a/src/assets/foxrenderfarm-arch-linux.png b/src/assets/foxrenderfarm-arch-linux.png deleted file mode 100644 index f3e9280..0000000 Binary files a/src/assets/foxrenderfarm-arch-linux.png and /dev/null differ diff --git a/src/assets/g-star-image.jpg b/src/assets/g-star-image.jpg deleted file mode 100644 index de9ed3b..0000000 Binary files a/src/assets/g-star-image.jpg and /dev/null differ diff --git a/src/assets/nicholai-closeup-portrait.JPEG b/src/assets/nicholai-closeup-portrait.JPEG deleted file mode 100644 index c9b0a61..0000000 Binary files a/src/assets/nicholai-closeup-portrait.JPEG and /dev/null differ diff --git a/src/assets/nicholai-medium-portrait.jpg b/src/assets/nicholai-medium-portrait.jpg deleted file mode 100644 index 60c4d17..0000000 Binary files a/src/assets/nicholai-medium-portrait.jpg and /dev/null differ diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro index d143b4e..f39db61 100644 --- a/src/components/BaseHead.astro +++ b/src/components/BaseHead.astro @@ -3,7 +3,7 @@ // all pages through the use of the component. import '../styles/global.css'; import type { ImageMetadata } from 'astro'; -import DefaultOGImage from '../assets/nicholai-medium-portrait.jpg'; +import DefaultOGImage from '../assets/nicholai-medium-portrait.avif'; import { SITE_TITLE, SITE_DESCRIPTION, SOCIAL_LINKS } from '../consts'; interface Props { diff --git a/src/components/sections/Hero.astro b/src/components/sections/Hero.astro index 0bf2b27..63997f1 100644 --- a/src/components/sections/Hero.astro +++ b/src/components/sections/Hero.astro @@ -1,6 +1,6 @@ --- import { Picture } from 'astro:assets'; -import heroPortrait from '../../assets/nicholai-closeup-portrait.JPEG'; +import heroPortrait from '../../assets/nicholai-closeup-portrait.avif'; interface Props { headlineLine1: string; diff --git a/src/utils/.env.example b/src/utils/.env.example new file mode 100644 index 0000000..2774e89 --- /dev/null +++ b/src/utils/.env.example @@ -0,0 +1,4 @@ +# OpenRouter API Configuration +# Get your API key from: https://openrouter.ai/keys + +OPENROUTER_API_KEY=your_api_key_here diff --git a/src/utils/README.md b/src/utils/README.md index a73bf3d..bd1b551 100644 --- a/src/utils/README.md +++ b/src/utils/README.md @@ -2,6 +2,93 @@ This directory contains utility scripts for the project. +## Git Commit Automation Script + +### git-commit.js + +Automatically generates commit messages using OpenRouter AI (inception/mercury-coder) based on your staged changes. The script analyzes both the git diff and status to create meaningful commit messages, then allows you to review, edit, and approve before committing. + +**Prerequisites:** +- OpenRouter API key (free to get started) + - Sign up: [openrouter.ai](https://openrouter.ai) + - Get your API key: [openrouter.ai/keys](https://openrouter.ai/keys) +- Create a `.env` file in `src/utils/` directory: + ```bash + # Copy the example file + cp src/utils/.env.example src/utils/.env + + # Edit the file and add your API key + OPENROUTER_API_KEY=your_actual_api_key_here + ``` + +**Usage:** + +```bash +# 1. Stage your changes +git add + +# 2. Run the commit script +pnpm commit + +# 3. Review the AI-generated message +# 4. Choose to [A]ccept, [E]dit, or [C]ancel +# 5. Optionally push to remote +``` + +**Workflow:** +1. **Staged Files Check** - Verifies you have changes staged +2. **Context Gathering** - Collects git diff and status +3. **AI Generation** - Calls OpenRouter API (inception/mercury-coder model) to generate commit message +4. **Review & Edit** - Shows message and prompts for approval +5. **Commit** - Creates the commit with approved message +6. **Optional Push** - Asks if you want to push to remote + +**Features:** +- AI-powered commit message generation +- Interactive approval process +- Message editing capability +- Optional automatic push +- Follows project commit message conventions +- Detailed error messages and help text + +**Example session:** +``` +🚀 Git Commit Automation + +🔍 Gathering git context... +🤖 Generating commit message with OpenRouter... + +📝 Generated commit message: +──────────────────────────────────────────────────────────── +Add git commit automation script with OpenRouter integration + +- Create interactive commit message generator using inception/mercury-coder +- Support message editing and approval workflow +- Add optional push to remote after commit +──────────────────────────────────────────────────────────── + +[A]ccept / [E]dit / [C]ancel? a + +📦 Creating commit... +✅ Commit created successfully! + +Push to remote? [y/N] y +🚀 Pushing to remote... +✅ Pushed successfully! + +✨ Done! +``` + +**Options:** +- `--help`, `-h` - Show help message + +**Troubleshooting:** +- If you get ".env file not found" error, create `src/utils/.env` with your OpenRouter API key +- Get your API key from: https://openrouter.ai/keys +- If you get "401 Unauthorized", check that your API key is correct in the `.env` file +- The script requires staged changes - use `git add` first +- Make sure `.env` is git-ignored (already configured in `.gitignore`) + ## Image Conversion Script ### convert-to-avif.js diff --git a/src/utils/git-commit.js b/src/utils/git-commit.js new file mode 100644 index 0000000..577ad3e --- /dev/null +++ b/src/utils/git-commit.js @@ -0,0 +1,464 @@ +#!/usr/bin/env node + +/** + * Git Commit Automation Script + * + * Automatically generates commit messages using OpenRouter AI (inception/mercury-coder) + * based on staged changes. Supports message editing and optional pushing. + * + * Usage: + * 1. Stage your changes: git add + * 2. Run: pnpm commit + * 3. Review/edit the generated message + * 4. Approve and optionally push + */ + +import { execSync, spawnSync } from 'child_process'; +import { createInterface } from 'readline'; +import { readFileSync, writeFileSync, unlinkSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { tmpdir } from 'os'; + +// Get current directory for ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load environment variables from .env file +function loadEnv() { + try { + const envPath = join(__dirname, '.env'); + const envContent = readFileSync(envPath, 'utf-8'); + const lines = envContent.split('\n'); + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + + const [key, ...valueParts] = trimmed.split('='); + const value = valueParts.join('=').trim(); + + if (key && value) { + process.env[key.trim()] = value.replace(/^["']|["']$/g, ''); + } + } + } catch (error) { + console.error(`${colors.red}❌ Failed to load .env file${colors.reset}`); + console.error(`${colors.yellow}💡 Create a .env file in src/utils/ with:${colors.reset}`); + console.error(` ${colors.dim}OPENROUTER_API_KEY=your_api_key_here${colors.reset}\n`); + process.exit(1); + } +} + +// Configuration +const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1'; +const MODEL_NAME = 'inception/mercury-coder'; + +// Color codes for terminal output +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', +}; + +/** + * Execute a git command and return the output + */ +function git(command, silent = false) { + try { + return execSync(`git ${command}`, { + encoding: 'utf-8', + stdio: silent ? 'pipe' : ['pipe', 'pipe', 'pipe'] + }).trim(); + } catch (error) { + if (!silent) { + console.error(`${colors.red}❌ Git command failed: ${command}${colors.reset}`); + console.error(error.message); + } + return null; + } +} + +/** + * Check if there are staged changes + */ +function checkStagedChanges() { + const stagedFiles = git('diff --staged --name-only', true); + return stagedFiles && stagedFiles.length > 0; +} + +/** + * Get git context for AI commit message generation + */ +function getGitContext() { + console.log(`${colors.cyan}🔍 Gathering git context...${colors.reset}`); + + const status = git('status --short'); + const diff = git('diff --staged'); + const stagedFiles = git('diff --staged --name-only'); + + return { + status, + diff, + stagedFiles + }; +} + +/** + * Call OpenRouter API to generate commit message + */ +async function generateCommitMessage(context) { + console.log(`${colors.cyan}🤖 Generating commit message with OpenRouter...${colors.reset}`); + + const systemPrompt = `You are a helpful assistant that generates concise, clear git commit messages. + +Generate commit messages following these guidelines: +- Use imperative mood (e.g., "Add", "Fix", "Update", "Refactor") +- Keep it concise but descriptive +- First line should be a short summary (50-72 characters) +- If needed, add a blank line and then bullet points for details +- Focus on WHAT changed and WHY, not HOW + +Generate ONLY the commit message, nothing else. Do not include any explanations or meta-commentary.`; + + const userPrompt = `Based on the following git changes, generate a commit message: + +Staged files: +${context.stagedFiles} + +Git status: +${context.status} + +Git diff: +${context.diff.slice(0, 8000)}${context.diff.length > 8000 ? '\n... (diff truncated)' : ''}`; + + try { + const apiKey = process.env.OPENROUTER_API_KEY; + + if (!apiKey) { + throw new Error('OPENROUTER_API_KEY not found in environment variables'); + } + + const response = await fetch(`${OPENROUTER_API_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}`, + 'HTTP-Referer': 'https://github.com/yourusername/git-commit-automation', + 'X-Title': 'Git Commit Automation', + }, + body: JSON.stringify({ + model: MODEL_NAME, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt } + ], + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(`OpenRouter API error: ${response.status} ${response.statusText}\n${JSON.stringify(errorData, null, 2)}`); + } + + const data = await response.json(); + + if (!data.choices || !data.choices[0] || !data.choices[0].message) { + throw new Error('Unexpected API response format'); + } + + return data.choices[0].message.content.trim(); + } catch (error) { + console.error(`${colors.red}❌ Failed to generate commit message${colors.reset}`); + console.error(error.message); + + // Check for common errors + if (error.message.includes('OPENROUTER_API_KEY not found')) { + console.log(`\n${colors.yellow}💡 Make sure you have a .env file in src/utils/ with:${colors.reset}`); + console.log(` ${colors.dim}OPENROUTER_API_KEY=your_api_key_here${colors.reset}`); + console.log(`\n${colors.yellow}💡 Get your API key from:${colors.reset}`); + console.log(` ${colors.dim}https://openrouter.ai/keys${colors.reset}`); + } else if (error.message.includes('ECONNREFUSED') || error.message.includes('fetch failed')) { + console.log(`\n${colors.yellow}💡 Check your internet connection${colors.reset}`); + } else if (error.message.includes('401')) { + console.log(`\n${colors.yellow}💡 Invalid API key. Check your OPENROUTER_API_KEY in .env${colors.reset}`); + } + + process.exit(1); + } +} + +/** + * Create readline interface for user input + */ +function createReadlineInterface() { + return createInterface({ + input: process.stdin, + output: process.stdout, + }); +} + +/** + * Ask user a question and get input + */ +function question(rl, query) { + return new Promise((resolve) => { + rl.question(query, resolve); + }); +} + +/** + * Open neovim to edit the commit message + */ +function editInNeovim(message) { + // Create a temporary file for editing + const tempFile = join(tmpdir(), `git-commit-${Date.now()}.txt`); + + try { + // Write the current message to the temp file + writeFileSync(tempFile, message, 'utf-8'); + + console.log(`\n${colors.cyan}✏️ Opening neovim to edit commit message...${colors.reset}`); + + // Open neovim with the temp file + const result = spawnSync('nvim', [tempFile], { + stdio: 'inherit', + shell: false + }); + + if (result.error) { + throw new Error(`Failed to open neovim: ${result.error.message}`); + } + + // Read the edited content + const editedMessage = readFileSync(tempFile, 'utf-8').trim(); + + // Clean up temp file + unlinkSync(tempFile); + + return editedMessage; + } catch (error) { + // Clean up temp file if it exists + try { + unlinkSync(tempFile); + } catch {} + + console.error(`${colors.red}❌ Failed to edit in neovim${colors.reset}`); + console.error(error.message); + + if (error.message.includes('Failed to open neovim')) { + console.log(`\n${colors.yellow}💡 Make sure neovim is installed:${colors.reset}`); + console.log(` ${colors.dim}# Arch Linux${colors.reset}`); + console.log(` ${colors.dim}sudo pacman -S neovim${colors.reset}`); + } + + // Return the original message if editing fails + return message; + } +} + +/** + * Display the commit message and get user approval + */ +async function getUserApproval(message, rl) { + console.log(`\n${colors.bright}${colors.green}📝 Generated commit message:${colors.reset}`); + console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`); + console.log(message); + console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}\n`); + + while (true) { + const answer = await question( + rl, + `${colors.yellow}[A]ccept / [E]dit / [C]ancel?${colors.reset} ` + ); + + const choice = answer.trim().toLowerCase(); + + if (choice === 'a' || choice === 'accept') { + return { approved: true, message }; + } else if (choice === 'e' || choice === 'edit') { + // Close readline to give full control to neovim + rl.pause(); + + // Open neovim for editing + const editedMessage = editInNeovim(message); + + // Resume readline + rl.resume(); + + // Show the edited message and ask for approval again + return getUserApproval(editedMessage, rl); + } else if (choice === 'c' || choice === 'cancel') { + return { approved: false, message: null }; + } else { + console.log(`${colors.red}Invalid option. Please enter A, E, or C.${colors.reset}`); + } + } +} + +/** + * Create the commit with the approved message + */ +function createCommit(message) { + console.log(`\n${colors.cyan}📦 Creating commit...${colors.reset}`); + + try { + // Use a temporary file for the commit message to handle multi-line messages + execSync(`git commit -F -`, { + input: message, + encoding: 'utf-8', + stdio: ['pipe', 'inherit', 'inherit'] + }); + + console.log(`${colors.green}✅ Commit created successfully!${colors.reset}`); + return true; + } catch (error) { + console.error(`${colors.red}❌ Failed to create commit${colors.reset}`); + console.error(error.message); + return false; + } +} + +/** + * Ask if user wants to push to remote + */ +async function askToPush(rl) { + const answer = await question( + rl, + `\n${colors.yellow}Push to remote? [y/N]${colors.reset} ` + ); + + return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes'; +} + +/** + * Push to remote repository + */ +function pushToRemote() { + console.log(`${colors.cyan}🚀 Pushing to remote...${colors.reset}`); + + try { + // Get current branch + const branch = git('rev-parse --abbrev-ref HEAD'); + + execSync(`git push origin ${branch}`, { + encoding: 'utf-8', + stdio: 'inherit' + }); + + console.log(`${colors.green}✅ Pushed successfully!${colors.reset}`); + return true; + } catch (error) { + console.error(`${colors.red}❌ Failed to push${colors.reset}`); + console.error(error.message); + return false; + } +} + +/** + * Show help message + */ +function showHelp() { + console.log(` +${colors.bright}Git Commit Automation Script${colors.reset} +${colors.dim}Generates commit messages using OpenRouter AI${colors.reset} + +${colors.bright}Usage:${colors.reset} + 1. Stage your changes: + ${colors.cyan}git add ${colors.reset} + + 2. Run this script: + ${colors.cyan}pnpm commit${colors.reset} + + 3. Review the AI-generated commit message + + 4. Choose to accept, edit, or cancel + + 5. Optionally push to remote + +${colors.bright}Requirements:${colors.reset} + - OpenRouter API key in .env file + - Create ${colors.dim}src/utils/.env${colors.reset} with: + ${colors.dim}OPENROUTER_API_KEY=your_api_key_here${colors.reset} + - Get your key from: ${colors.dim}https://openrouter.ai/keys${colors.reset} + +${colors.bright}Options:${colors.reset} + --help, -h Show this help message +`); +} + +/** + * Main function + */ +async function main() { + // Check for help flag + const args = process.argv.slice(2); + if (args.includes('--help') || args.includes('-h')) { + showHelp(); + process.exit(0); + } + + // Load environment variables + loadEnv(); + + console.log(`${colors.bright}${colors.blue}🚀 Git Commit Automation${colors.reset}\n`); + + // Check if we're in a git repository + if (!git('rev-parse --git-dir', true)) { + console.error(`${colors.red}❌ Not a git repository${colors.reset}`); + process.exit(1); + } + + // Check for staged changes + if (!checkStagedChanges()) { + console.error(`${colors.red}❌ No staged changes found${colors.reset}`); + console.log(`\n${colors.yellow}💡 Stage your changes first:${colors.reset}`); + console.log(` ${colors.dim}git add ${colors.reset}\n`); + process.exit(1); + } + + // Get git context + const context = getGitContext(); + + // Generate commit message using OpenRouter + const generatedMessage = await generateCommitMessage(context); + + // Get user approval + const rl = createReadlineInterface(); + const { approved, message } = await getUserApproval(generatedMessage, rl); + + if (!approved) { + console.log(`\n${colors.yellow}⏭️ Commit cancelled${colors.reset}`); + rl.close(); + process.exit(0); + } + + // Create the commit + const commitSuccess = createCommit(message); + + if (!commitSuccess) { + rl.close(); + process.exit(1); + } + + // Ask to push + const shouldPush = await askToPush(rl); + rl.close(); + + if (shouldPush) { + pushToRemote(); + } + + console.log(`\n${colors.green}✨ Done!${colors.reset}\n`); +} + +// Run the script +main().catch((error) => { + console.error(`${colors.red}❌ Unexpected error:${colors.reset}`, error); + process.exit(1); +});