Repository navigation
Expand file tree
/
Copy pathconfig.ts
More file actions
212 lines (177 loc) · 5.72 KB
/
Copy pathconfig.ts
File metadata and controls
212 lines (177 loc) · 5.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import ini from 'ini';
import { fileExists } from './fs.js';
import { KnownError } from './error.js';
const commitTypes = ['', 'conventional'] as const;
export type CommitType = (typeof commitTypes)[number];
const { hasOwnProperty } = Object.prototype;
export const hasOwn = (object: unknown, key: PropertyKey) =>
hasOwnProperty.call(object, key);
const parseAssert = (name: string, condition: any, message: string) => {
if (!condition) {
throw new KnownError(`Invalid config property ${name}: ${message}`);
}
};
const apiKey = (name: string) => (key?: string) => {
if (!key) return undefined;
parseAssert(name, /^\S+$/.test(key), 'Cannot contain whitespace');
return key;
};
const configParsers = {
OPENAI_API_KEY: apiKey('OPENAI_API_KEY'),
GOOGLE_API_KEY: apiKey('GOOGLE_API_KEY'),
ANTHROPIC_API_KEY: apiKey('ANTHROPIC_API_KEY'),
MOONSHOT_API_KEY: apiKey('MOONSHOT_API_KEY'),
DEEPSEEK_API_KEY: apiKey('DEEPSEEK_API_KEY'),
ZHIPU_API_KEY: apiKey('ZHIPU_API_KEY'),
MINIMAX_API_KEY: apiKey('MINIMAX_API_KEY'),
GROQ_API_KEY(key?: string) {
if (!key) return undefined;
parseAssert('GROQ_API_KEY', key.startsWith('gsk_'), 'Must start with "gsk_"');
return key;
},
locale(locale?: string) {
if (!locale) {
return 'en';
}
parseAssert('locale', locale, 'Cannot be empty');
parseAssert(
'locale',
/^[a-z]{2,8}(?:[-_][a-z0-9]{2,8})*$/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(count?: string) {
if (!count) {
return 1;
}
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 <= 5, 'Must be less or equal to 5');
return parsed;
},
type(type?: string) {
if (!type) {
return '';
}
parseAssert(
'type',
commitTypes.includes(type as CommitType),
'Invalid commit type'
);
return type as CommitType;
},
proxy(url?: string) {
if (!url || url.length === 0) {
return undefined;
}
let valid = false;
try { valid = ['http:', 'https:'].includes(new URL(url).protocol); } catch {}
parseAssert('proxy', valid, 'Must be a valid HTTP/HTTPS URL');
return url;
},
scope(value = '') {
parseAssert('scope', value === '' || /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,39}$/.test(value), 'Use up to 40 letters, digits, dots, slashes, underscores or hyphens');
return value;
},
context(value = '') {
parseAssert('context', value.length <= 2000 && !/[\r\n\x00]/.test(value), 'Use one line of at most 2000 characters');
return value;
},
'max-diff-chars'(value = '16000') {
const parsed = Number(value);
parseAssert('max-diff-chars', /^\d+$/.test(value) && parsed >= 1000 && parsed <= 100000, 'Must be an integer between 1000 and 100000');
return parsed;
},
model(model?: string) {
if (!model) {
return '';
}
parseAssert('model', /^\S+$/.test(model), 'Cannot contain whitespace');
return model;
},
timeout(timeout?: string) {
if (!timeout) {
return 10_000;
}
parseAssert('timeout', /^\d+$/.test(timeout), 'Must be an integer');
const parsed = Number(timeout);
parseAssert('timeout', Number.isSafeInteger(parsed) && parsed >= 500 && parsed <= 300_000, 'Must be between 500 and 300000ms');
return parsed;
},
'max-length'(maxLength?: string) {
if (!maxLength) {
return 100;
}
parseAssert('max-length', /^\d+$/.test(maxLength), 'Must be an integer');
const parsed = Number(maxLength);
parseAssert(
'max-length',
parsed >= 20,
'Must be greater than 20 characters'
);
parseAssert(
'max-length',
parsed <= 200,
'Must be less than or equal to 200 characters'
);
return parsed;
},
} as const;
type ConfigKeys = keyof typeof configParsers;
export type RawConfig = {
[key in ConfigKeys]?: string;
};
export type ValidConfig = {
[Key in ConfigKeys]: ReturnType<(typeof configParsers)[Key]>;
};
const configPath = path.join(os.homedir(), '.lazycommit');
const readConfigFile = async (): Promise<RawConfig> => {
const configExists = await fileExists(configPath);
if (!configExists) {
return Object.create(null);
}
const configString = await fs.readFile(configPath, 'utf8');
return ini.parse(configString);
};
export const getConfig = async (
cliConfig?: RawConfig,
suppressErrors?: boolean
): Promise<ValidConfig> => {
const config = await readConfigFile();
const parsedConfig: Record<string, unknown> = {};
for (const key of Object.keys(configParsers) as ConfigKeys[]) {
const parser = configParsers[key];
const value = cliConfig?.[key] ?? config[key];
const input = value === undefined ? undefined : String(value);
if (suppressErrors) {
try {
parsedConfig[key] = parser(input);
} catch {}
} else {
parsedConfig[key] = parser(input);
}
}
if (!suppressErrors && parsedConfig.scope && parsedConfig.type !== 'conventional') {
throw new KnownError('A scope requires --type conventional.');
}
return parsedConfig as ValidConfig;
};
export const setConfigs = async (keyValues: [key: string, value: string][]) => {
const config = await readConfigFile();
for (const [key, value] of keyValues) {
if (!hasOwn(configParsers, key)) {
throw new KnownError(`Invalid config property: ${key}`);
}
if (typeof value !== 'string') throw new KnownError(`Expected ${key}=<value>`);
const parsed = configParsers[key as ConfigKeys](value);
if (parsed === undefined) delete config[key as ConfigKeys];
else config[key as ConfigKeys] = parsed as any;
}
await fs.writeFile(configPath, ini.stringify(config), { encoding: 'utf8', mode: 0o600 });
await fs.chmod(configPath, 0o600);
};