feat: config command (#71)
This commit is contained in:
63
src/utils/config.ts
Normal file
63
src/utils/config.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import ini from 'ini';
|
||||
|
||||
const keyValidators = {
|
||||
OPENAI_KEY(key: string) {
|
||||
if (!key) {
|
||||
return 'Cannot be empty';
|
||||
}
|
||||
|
||||
if (!key.startsWith('sk-')) {
|
||||
return 'Must start with "sk-"';
|
||||
}
|
||||
|
||||
if (key.length !== 51) {
|
||||
return 'Must be 51 characters long';
|
||||
}
|
||||
},
|
||||
} as const;
|
||||
|
||||
type ValidKeys = keyof typeof keyValidators;
|
||||
type ConfigType = {
|
||||
[key in ValidKeys]?: string;
|
||||
};
|
||||
|
||||
const configPath = path.join(os.homedir(), '.aicommits');
|
||||
|
||||
const fileExists = (filePath: string) => fs.access(filePath).then(() => true, () => false);
|
||||
|
||||
export const getConfig = async (): Promise<ConfigType> => {
|
||||
const configExists = await fileExists(configPath);
|
||||
if (!configExists) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const configString = await fs.readFile(configPath, 'utf8');
|
||||
return ini.parse(configString);
|
||||
};
|
||||
|
||||
const { hasOwnProperty } = Object.prototype;
|
||||
const hasOwn = (object: unknown, key: PropertyKey) => hasOwnProperty.call(object, key);
|
||||
|
||||
export const setConfigs = async (
|
||||
keyValues: [key: string, value: string][],
|
||||
) => {
|
||||
const config = await getConfig();
|
||||
|
||||
for (const [key, value] of keyValues) {
|
||||
if (!hasOwn(keyValidators, key)) {
|
||||
throw new Error(`Invalid config property: ${key}`);
|
||||
}
|
||||
|
||||
const isInvalid = keyValidators[key as ValidKeys](value);
|
||||
if (isInvalid) {
|
||||
throw new Error(`Invalid value for ${key}: ${isInvalid}`);
|
||||
}
|
||||
|
||||
config[key as ValidKeys] = value;
|
||||
}
|
||||
|
||||
await fs.writeFile(configPath, ini.stringify(config), 'utf8');
|
||||
};
|
||||
39
src/utils/git.ts
Normal file
39
src/utils/git.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { execa } from 'execa';
|
||||
|
||||
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!');
|
||||
}
|
||||
};
|
||||
|
||||
const excludeFromDiff = [
|
||||
'package-lock.json',
|
||||
'yarn.lock',
|
||||
'pnpm-lock.yaml',
|
||||
].map(file => `:(exclude)${file}`);
|
||||
|
||||
export const getStagedDiff = async () => {
|
||||
const diffCached = ['diff', '--cached'];
|
||||
const { stdout: files } = await execa(
|
||||
'git',
|
||||
[...diffCached, '--name-only', ...excludeFromDiff],
|
||||
);
|
||||
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { stdout: diff } = await execa(
|
||||
'git',
|
||||
[...diffCached, ...excludeFromDiff],
|
||||
);
|
||||
|
||||
return {
|
||||
files: files.split('\n'),
|
||||
diff,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDetectedMessage = (files: string[]) => `Detected ${files.length.toLocaleString()} staged file${files.length > 1 ? 's' : ''}`;
|
||||
41
src/utils/openai.ts
Normal file
41
src/utils/openai.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Configuration, OpenAIApi } from 'openai';
|
||||
|
||||
const sanitizeMessage = (message: string) => message.trim().replace(/[\n\r]/g, '').replace(/(\w)\.$/, '$1');
|
||||
|
||||
const promptTemplate = 'Write an insightful but concise Git commit message in a complete sentence in present tense for the following diff without prefacing it with anything:';
|
||||
|
||||
export const generateCommitMessage = async (
|
||||
apiKey: string,
|
||||
diff: string,
|
||||
completions: number,
|
||||
) => {
|
||||
const prompt = `${promptTemplate}\n${diff}`;
|
||||
|
||||
// Accounting for GPT-3's input req of 4k tokens (approx 8k chars)
|
||||
if (prompt.length > 8000) {
|
||||
throw new Error('The diff is too large for the OpenAI API');
|
||||
}
|
||||
|
||||
const openai = new OpenAIApi(new Configuration({ apiKey }));
|
||||
try {
|
||||
const completion = await openai.createCompletion({
|
||||
model: 'text-davinci-003',
|
||||
prompt,
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
frequency_penalty: 0,
|
||||
presence_penalty: 0,
|
||||
max_tokens: 200,
|
||||
stream: false,
|
||||
n: completions,
|
||||
});
|
||||
|
||||
return completion.data.choices
|
||||
.filter(choice => choice.text)
|
||||
.map(choice => sanitizeMessage(choice.text!));
|
||||
} catch (error) {
|
||||
const errorAsAny = error as any;
|
||||
errorAsAny.message = `OpenAI API Error: ${errorAsAny.message} - ${errorAsAny.response.statusText}`;
|
||||
throw errorAsAny;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user