diff --git a/src/commands/aicommits.ts b/src/commands/aicommits.ts index 0961f3a3..2ed074c5 100644 --- a/src/commands/aicommits.ts +++ b/src/commands/aicommits.ts @@ -12,6 +12,7 @@ import { } from '../utils/git.js'; import { getConfig } from '../utils/config.js'; import { generateCommitMessage } from '../utils/openai.js'; +import { KnownError, handleCliError } from '../utils/error.js'; export default async ( generate: number, @@ -26,7 +27,7 @@ export default async ( const staged = await getStagedDiff(); if (!staged) { - throw new Error('No staged changes found. Make sure to stage your changes with `git add`.'); + throw new KnownError('No staged changes found. Make sure to stage your changes with `git add`.'); } detectingFiles.stop(`${getDetectedMessage(staged.files)}:\n${ @@ -36,7 +37,7 @@ export default async ( const config = await getConfig(); const OPENAI_KEY = process.env.OPENAI_KEY ?? process.env.OPENAI_API_KEY ?? config.OPENAI_KEY; if (!OPENAI_KEY) { - throw new Error('Please set your OpenAI API key in ~/.aicommits'); + throw new KnownError('Please set your OpenAI API key via `aicommits config set OPENAI_KEY=`'); } const s = spinner(); @@ -78,5 +79,6 @@ export default async ( outro(`${green('✔')} Successfully committed!`); })().catch((error) => { outro(`${red('✖')} ${error.message}`); + handleCliError(error); process.exit(1); }); diff --git a/src/commands/config.ts b/src/commands/config.ts index 85c742a4..5ca74d40 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,6 +1,7 @@ import { command } from 'cleye'; import { red } from 'kolorist'; import { getConfig, setConfigs } from '../utils/config.js'; +import { KnownError, handleCliError } from '../utils/error.js'; export default command({ name: 'config', @@ -25,9 +26,10 @@ export default command({ return; } - throw new Error(`Invalid mode: ${mode}`); + throw new KnownError(`Invalid mode: ${mode}`); })().catch((error) => { console.error(`${red('✖')} ${error.message}`); + handleCliError(error); process.exit(1); }); }); diff --git a/src/commands/hook.ts b/src/commands/hook.ts index 45563b1a..309b5958 100644 --- a/src/commands/hook.ts +++ b/src/commands/hook.ts @@ -5,6 +5,7 @@ import { green, red } from 'kolorist'; import { command } from 'cleye'; import { assertGitRepo } from '../utils/git.js'; import { fileExists } from '../utils/fs.js'; +import { KnownError, handleCliError } from '../utils/error.js'; const hookName = 'prepare-commit-msg'; const symlinkPath = `.git/hooks/${hookName}`; @@ -32,7 +33,7 @@ export default command({ console.warn('The hook is already installed'); return; } - throw new Error(`A different ${hookName} hook seems to be installed. Please remove it before installing aicommits.`); + throw new KnownError(`A different ${hookName} hook seems to be installed. Please remove it before installing aicommits.`); } await fs.mkdir(path.dirname(symlinkPath), { recursive: true }); @@ -58,9 +59,10 @@ export default command({ return; } - throw new Error(`Invalid mode: ${mode}`); + throw new KnownError(`Invalid mode: ${mode}`); })().catch((error) => { console.error(`${red('✖')} ${error.message}`); + handleCliError(error); process.exit(1); }); }); diff --git a/src/commands/prepare-commit-msg-hook.ts b/src/commands/prepare-commit-msg-hook.ts index c675b58b..ba05292e 100644 --- a/src/commands/prepare-commit-msg-hook.ts +++ b/src/commands/prepare-commit-msg-hook.ts @@ -8,12 +8,13 @@ import { import { getStagedDiff } from '../utils/git.js'; import { getConfig } from '../utils/config.js'; import { generateCommitMessage } from '../utils/openai.js'; +import { KnownError, handleCliError } from '../utils/error.js'; const [messageFilePath, commitSource] = process.argv.slice(2); export default () => (async () => { if (!messageFilePath) { - throw new Error('Commit message file path is missing. This file should be called from the "prepare-commit-msg" git hook'); + throw new KnownError('Commit message file path is missing. This file should be called from the "prepare-commit-msg" git hook'); } // If a commit message is passed in, ignore @@ -31,7 +32,7 @@ export default () => (async () => { const { OPENAI_KEY, generate } = await getConfig(); if (!OPENAI_KEY) { - throw new Error('Please set your OpenAI API key in ~/.aicommits'); + throw new KnownError('Please set your OpenAI API key in ~/.aicommits'); } const s = spinner(); @@ -61,5 +62,6 @@ export default () => (async () => { outro(`${green('✔')} Saved commit message!`); })().catch((error) => { outro(`${red('✖')} ${error.message}`); + handleCliError(error); process.exit(1); }); diff --git a/src/utils/config.ts b/src/utils/config.ts index 4d341ce5..ba206182 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -3,6 +3,7 @@ import path from 'path'; import os from 'os'; import ini from 'ini'; import { fileExists } from './fs.js'; +import { KnownError } from './error.js'; const parseAssert = ( name: string, @@ -10,7 +11,7 @@ const parseAssert = ( message: string, ) => { if (!condition) { - throw new Error(`Invalid config property ${name}: ${message}`); + throw new KnownError(`Invalid config property ${name}: ${message}`); } }; @@ -67,7 +68,7 @@ export const setConfigs = async ( for (const [key, value] of keyValues) { if (!hasOwn(configParsers, key)) { - throw new Error(`Invalid config property: ${key}`); + throw new KnownError(`Invalid config property: ${key}`); } const parsed = configParsers[key as ValidKeys](value); diff --git a/src/utils/error.ts b/src/utils/error.ts new file mode 100644 index 00000000..3cfd1296 --- /dev/null +++ b/src/utils/error.ts @@ -0,0 +1,20 @@ +import { dim } from 'kolorist'; +import { version } from '../../package.json'; + +export class KnownError extends Error {} + +const indent = ' '; + +export const handleCliError = (error: any) => { + if ( + error instanceof Error + && !(error instanceof KnownError) + ) { + if (error.stack) { + console.error(dim(error.stack.split('\n').slice(1).join('\n'))); + } + console.error(`\n${indent}${dim(`aicommits v${version}`)}`); + console.error(`\n${indent}Please open a Bug report with the information above:`); + console.error(`${indent}https://github.com/Nutlope/aicommits/issues/new/choose`); + } +}; diff --git a/src/utils/git.ts b/src/utils/git.ts index bc455c17..943f91df 100644 --- a/src/utils/git.ts +++ b/src/utils/git.ts @@ -1,10 +1,11 @@ import { execa } from 'execa'; +import { KnownError } from './error.js'; export const assertGitRepo = async () => { const { stdout } = await execa('git', ['rev-parse', '--is-inside-work-tree'], { reject: false }); if (stdout !== 'true') { - throw new Error('The current directory must be a Git repository!'); + throw new KnownError('The current directory must be a Git repository!'); } }; diff --git a/src/utils/openai.ts b/src/utils/openai.ts index 0b956247..1c3191f3 100644 --- a/src/utils/openai.ts +++ b/src/utils/openai.ts @@ -1,6 +1,7 @@ import https from 'https'; import type { CreateCompletionRequest, CreateCompletionResponse } from 'openai'; import { encoding_for_model as encodingForModel } from '@dqbd/tiktoken'; +import { KnownError } from './error.js'; const createCompletion = ( apiKey: string, @@ -31,7 +32,7 @@ const createCompletion = ( errorMessage += '; Check the API status: https://status.openai.com'; } - return reject(new Error(errorMessage)); + return reject(new KnownError(errorMessage)); } const body: Buffer[] = []; @@ -46,7 +47,7 @@ const createCompletion = ( request.on('error', reject); request.on('timeout', () => { request.destroy(); - reject(new Error('Request timed out')); + reject(new KnownError('Request timed out')); }); request.write(postContent); @@ -74,7 +75,7 @@ export const generateCommitMessage = async ( * https://platform.openai.com/docs/models/overview#:~:text=to%20Sep%202021-,text%2Ddavinci%2D003,-Can%20do%20any */ if (encoder.encode(prompt).length > 4000) { - throw new Error('The diff is too large for the OpenAI API. Try reducing the number of staged changes, or write your own commit message.'); + throw new KnownError('The diff is too large for the OpenAI API. Try reducing the number of staged changes, or write your own commit message.'); } try { @@ -97,7 +98,7 @@ export const generateCommitMessage = async ( } catch (error) { const errorAsAny = error as any; if (errorAsAny.code === 'ENOTFOUND') { - throw new Error(`Error connecting to ${errorAsAny.hostname} (${errorAsAny.syscall}). Are you connected to the internet?`); + throw new KnownError(`Error connecting to ${errorAsAny.hostname} (${errorAsAny.syscall}). Are you connected to the internet?`); } throw errorAsAny;