refactor: reuse config validators on cli config

This commit is contained in:
Hiroki Osame
2023-03-09 21:50:52 +09:00
parent 58ce61eab8
commit be462f6498
4 changed files with 57 additions and 46 deletions

View File

@@ -21,9 +21,8 @@ cli(
flags: { flags: {
generate: { generate: {
type: Number, type: Number,
description: 'Number of messages to generate. (Warning: generating multiple costs more)', description: 'Number of messages to generate. (Warning: generating multiple costs more) (default: 1)',
alias: 'g', alias: 'g',
default: 1,
}, },
}, },

View File

@@ -15,7 +15,7 @@ import { generateCommitMessage } from '../utils/openai.js';
import { KnownError, handleCliError } from '../utils/error.js'; import { KnownError, handleCliError } from '../utils/error.js';
export default async ( export default async (
generate: number, generate: number | undefined,
rawArgv: string[], rawArgv: string[],
) => (async () => { ) => (async () => {
intro(bgCyan(black(' aicommits '))); intro(bgCyan(black(' aicommits ')));
@@ -34,20 +34,18 @@ export default async (
staged.files.map(file => ` ${file}`).join('\n') staged.files.map(file => ` ${file}`).join('\n')
}`); }`);
const config = await getConfig(); const config = await getConfig({
const OPENAI_KEY = process.env.OPENAI_KEY ?? process.env.OPENAI_API_KEY ?? config.OPENAI_KEY; OPENAI_KEY: process.env.OPENAI_KEY ?? process.env.OPENAI_API_KEY,
const locale = config.locale ?? 'en'; generate: generate?.toString(),
if (!OPENAI_KEY) { });
throw new KnownError('Please set your OpenAI API key via `aicommits config set OPENAI_KEY=<your token>`');
}
const s = spinner(); const s = spinner();
s.start('The AI is analyzing your changes'); s.start('The AI is analyzing your changes');
const messages = await generateCommitMessage( const messages = await generateCommitMessage(
OPENAI_KEY, config.OPENAI_KEY,
locale, config.locale,
staged.diff, staged.diff,
generate, config.generate,
); );
s.stop('Changes analyzed'); s.stop('Changes analyzed');

View File

@@ -30,18 +30,15 @@ export default () => (async () => {
intro(bgCyan(black(' aicommits '))); intro(bgCyan(black(' aicommits ')));
const { OPENAI_KEY, locale = 'en', generate } = await getConfig(); const config = await getConfig();
if (!OPENAI_KEY) {
throw new KnownError('Please set your OpenAI API key in ~/.aicommits');
}
const s = spinner(); const s = spinner();
s.start('The AI is analyzing your changes'); s.start('The AI is analyzing your changes');
const messages = await generateCommitMessage( const messages = await generateCommitMessage(
OPENAI_KEY, config.OPENAI_KEY,
locale, config.locale,
staged!.diff, staged!.diff,
generate || 1, config.generate,
); );
s.stop('Changes analyzed'); s.stop('Changes analyzed');

View File

@@ -19,24 +19,33 @@ const parseAssert = (
}; };
const configParsers = { const configParsers = {
OPENAI_KEY(key: string) { OPENAI_KEY(key?: string) {
parseAssert('OPENAI_KEY', key, 'Cannot be empty'); if (!key) {
throw new KnownError('Please set your OpenAI API key via `aicommits config set OPENAI_KEY=<your token>`');
}
parseAssert('OPENAI_KEY', key.startsWith('sk-'), 'Must start with "sk-"'); parseAssert('OPENAI_KEY', key.startsWith('sk-'), 'Must start with "sk-"');
parseAssert('OPENAI_KEY', key.length === 51, 'Must be 51 characters long'); parseAssert('OPENAI_KEY', key.length === 51, 'Must be 51 characters long');
return key; return key;
}, },
locale(key: string) { locale(locale?: string) {
parseAssert('locale', key, 'Cannot be empty'); if (!locale) {
parseAssert('locale', /^[a-z-]+$/i.test(key), 'Must be a valid locale (letters and dashes/underscores). You can consult the list of codes in: https://wikipedia.org/wiki/List_of_ISO_639-1_codes'); return 'en';
}
return key; parseAssert('locale', locale, 'Cannot be empty');
parseAssert('locale', /^[a-z-]+$/i.test(locale), 'Must be a valid locale (letters and dashes/underscores). You can consult the list of codes in: https://wikipedia.org/wiki/List_of_ISO_639-1_codes');
return locale;
}, },
generate(key: string) { generate(count?: string) {
parseAssert('generate', key, 'Cannot be empty'); if (!count) {
parseAssert('generate', /^\d+$/.test(key), 'Must be an integer'); return 1;
}
const parsed = Number(key); parseAssert('generate', /^\d+$/.test(count), 'Must be an integer');
const parsed = Number(count);
parseAssert('generate', parsed > 0, 'Must be greater than 0'); parseAssert('generate', parsed > 0, 'Must be greater than 0');
parseAssert('generate', parsed <= 5, 'Must be less or equal to 5'); parseAssert('generate', parsed <= 5, 'Must be less or equal to 5');
@@ -44,45 +53,53 @@ const configParsers = {
}, },
} as const; } as const;
type ValidKeys = keyof typeof configParsers; type ConfigKeys = keyof typeof configParsers;
type ConfigType = {
[key in ValidKeys]?: ReturnType<typeof configParsers[key]>; type RawConfig = {
[key in ConfigKeys]?: string;
};
type ValidConfig = {
[Key in ConfigKeys]: ReturnType<typeof configParsers[Key]>;
}; };
const configPath = path.join(os.homedir(), '.aicommits'); const configPath = path.join(os.homedir(), '.aicommits');
export const getConfig = async (): Promise<ConfigType> => { const readConfigFile = async (): Promise<RawConfig> => {
const configExists = await fileExists(configPath); const configExists = await fileExists(configPath);
if (!configExists) { if (!configExists) {
return {}; return Object.create(null);
} }
const configString = await fs.readFile(configPath, 'utf8'); const configString = await fs.readFile(configPath, 'utf8');
const config = ini.parse(configString); return ini.parse(configString);
for (const key of Object.keys(config)) { };
if (hasOwn(configParsers, key)) {
const parsed = configParsers[key as ValidKeys](config[key]); export const getConfig = async (cliConfig?: RawConfig): Promise<ValidConfig> => {
config[key as ValidKeys] = parsed; const config = await readConfigFile();
} else { const parsedConfig: Record<string, unknown> = {};
console.warn(`\n⚠ Unknown config property "${key}" found in ${configPath}`);
} for (const key of Object.keys(configParsers) as ConfigKeys[]) {
const parser = configParsers[key];
const value = cliConfig?.[key] ?? config[key];
parsedConfig[key] = parser(value);
} }
return config; return parsedConfig as ValidConfig;
}; };
export const setConfigs = async ( export const setConfigs = async (
keyValues: [key: string, value: string][], keyValues: [key: string, value: string][],
) => { ) => {
const config = await getConfig(); const config = await readConfigFile();
for (const [key, value] of keyValues) { for (const [key, value] of keyValues) {
if (!hasOwn(configParsers, key)) { if (!hasOwn(configParsers, key)) {
throw new KnownError(`Invalid config property: ${key}`); throw new KnownError(`Invalid config property: ${key}`);
} }
const parsed = configParsers[key as ValidKeys](value); const parsed = configParsers[key as ConfigKeys](value);
config[key as ValidKeys] = parsed as any; config[key as ConfigKeys] = parsed as any;
} }
await fs.writeFile(configPath, ini.stringify(config), 'utf8'); await fs.writeFile(configPath, ini.stringify(config), 'utf8');