zencommit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +421 -0
  2. package/bin/zencommit.js +37 -0
  3. package/package.json +68 -0
  4. package/scripts/install.mjs +146 -0
  5. package/scripts/platform.mjs +34 -0
  6. package/src/auth/secrets.ts +234 -0
  7. package/src/commands/auth.ts +138 -0
  8. package/src/commands/config.ts +83 -0
  9. package/src/commands/default.ts +322 -0
  10. package/src/commands/models.ts +67 -0
  11. package/src/config/load.test.ts +47 -0
  12. package/src/config/load.ts +118 -0
  13. package/src/config/merge.test.ts +25 -0
  14. package/src/config/merge.ts +30 -0
  15. package/src/config/types.ts +119 -0
  16. package/src/config/validate.ts +139 -0
  17. package/src/git/commit.ts +17 -0
  18. package/src/git/diff.ts +89 -0
  19. package/src/git/repo.ts +10 -0
  20. package/src/index.ts +207 -0
  21. package/src/llm/generate.ts +188 -0
  22. package/src/llm/prompt-template.ts +44 -0
  23. package/src/llm/prompt.ts +83 -0
  24. package/src/llm/prompts/base.md +119 -0
  25. package/src/llm/prompts/conventional.md +123 -0
  26. package/src/llm/prompts/gitmoji.md +212 -0
  27. package/src/llm/prompts/system.md +21 -0
  28. package/src/llm/providers.ts +102 -0
  29. package/src/llm/tokens.test.ts +22 -0
  30. package/src/llm/tokens.ts +46 -0
  31. package/src/llm/truncate.test.ts +60 -0
  32. package/src/llm/truncate.ts +552 -0
  33. package/src/metadata/cache.ts +28 -0
  34. package/src/metadata/index.ts +94 -0
  35. package/src/metadata/providers/local.ts +66 -0
  36. package/src/metadata/providers/modelsdev.ts +145 -0
  37. package/src/metadata/types.ts +20 -0
  38. package/src/ui/editor.ts +33 -0
  39. package/src/ui/prompts.ts +99 -0
  40. package/src/util/exec.ts +57 -0
  41. package/src/util/fs.ts +46 -0
  42. package/src/util/logger.ts +50 -0
  43. package/src/util/redact.ts +30 -0
@@ -0,0 +1,34 @@
1
+ import path from 'node:path';
2
+
3
+ export function getPlatformId() {
4
+ const { platform, arch } = process;
5
+ if (platform === 'linux' && arch === 'x64') {
6
+ return 'linux-x64';
7
+ }
8
+ if (platform === 'darwin' && arch === 'x64') {
9
+ return 'darwin-x64';
10
+ }
11
+ if (platform === 'darwin' && arch === 'arm64') {
12
+ return 'darwin-arm64';
13
+ }
14
+ if (platform === 'win32' && arch === 'x64') {
15
+ return 'win32-x64';
16
+ }
17
+ throw new Error(`Unsupported platform: ${platform} ${arch}`);
18
+ }
19
+
20
+ export function getBinaryName() {
21
+ return process.platform === 'win32' ? 'zencommit.exe' : 'zencommit';
22
+ }
23
+
24
+ export function getAssetName(version) {
25
+ const platformId = getPlatformId();
26
+ const ext = process.platform === 'win32' ? '.exe' : '';
27
+ return `zencommit-${version}-${platformId}${ext}`;
28
+ }
29
+
30
+ export function getInstallPath(pkgRoot) {
31
+ const platformId = getPlatformId();
32
+ const binaryName = getBinaryName();
33
+ return path.join(pkgRoot, 'bin', platformId, binaryName);
34
+ }
@@ -0,0 +1,234 @@
1
+ export interface ProviderAuthConfig {
2
+ id: string;
3
+ name: string;
4
+ envKeys: string[];
5
+ required: boolean;
6
+ primaryEnvKey?: string;
7
+ providerIds?: string[];
8
+ }
9
+
10
+ const PROVIDER_AUTH_CONFIGS: ProviderAuthConfig[] = [
11
+ {
12
+ id: 'openai',
13
+ name: 'OpenAI',
14
+ envKeys: ['OPENAI_API_KEY'],
15
+ required: true,
16
+ },
17
+ {
18
+ id: 'anthropic',
19
+ name: 'Anthropic',
20
+ envKeys: ['ANTHROPIC_API_KEY'],
21
+ required: true,
22
+ primaryEnvKey: 'ANTHROPIC_API_KEY',
23
+ },
24
+ {
25
+ id: 'google',
26
+ name: 'Google Generative AI',
27
+ envKeys: ['GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY'],
28
+ required: true,
29
+ primaryEnvKey: 'GOOGLE_GENERATIVE_AI_API_KEY',
30
+ providerIds: ['google-generative-ai', 'gemini'],
31
+ },
32
+ {
33
+ id: 'vertex',
34
+ name: 'Google Vertex AI',
35
+ envKeys: ['GOOGLE_VERTEX_API_KEY'],
36
+ required: false,
37
+ providerIds: ['google-vertex', 'google-vertex-ai'],
38
+ },
39
+ {
40
+ id: 'vertex-anthropic',
41
+ name: 'Google Vertex Anthropic',
42
+ envKeys: ['GOOGLE_VERTEX_API_KEY'],
43
+ required: false,
44
+ providerIds: ['google-vertex-anthropic'],
45
+ },
46
+ {
47
+ id: 'xai',
48
+ name: 'xAI Grok',
49
+ envKeys: ['XAI_API_KEY'],
50
+ required: true,
51
+ providerIds: ['xai-grok', 'grok'],
52
+ },
53
+ {
54
+ id: 'vercel',
55
+ name: 'Vercel',
56
+ envKeys: ['VERCEL_API_KEY'],
57
+ required: true,
58
+ },
59
+ {
60
+ id: 'gateway',
61
+ name: 'Vercel AI Gateway',
62
+ envKeys: ['AI_GATEWAY_API_KEY'],
63
+ required: true,
64
+ providerIds: ['ai-gateway', 'vercel-ai-gateway'],
65
+ },
66
+ {
67
+ id: 'azure',
68
+ name: 'Azure OpenAI',
69
+ envKeys: ['AZURE_API_KEY'],
70
+ required: true,
71
+ providerIds: ['azure-openai'],
72
+ },
73
+ {
74
+ id: 'bedrock',
75
+ name: 'Amazon Bedrock',
76
+ envKeys: [
77
+ 'AWS_ACCESS_KEY_ID',
78
+ 'AWS_SECRET_ACCESS_KEY',
79
+ 'AWS_SESSION_TOKEN',
80
+ 'AWS_BEARER_TOKEN_BEDROCK',
81
+ ],
82
+ required: true,
83
+ providerIds: ['amazon-bedrock', 'aws-bedrock'],
84
+ },
85
+ {
86
+ id: 'groq',
87
+ name: 'Groq',
88
+ envKeys: ['GROQ_API_KEY'],
89
+ required: true,
90
+ },
91
+ {
92
+ id: 'deepinfra',
93
+ name: 'DeepInfra',
94
+ envKeys: ['DEEPINFRA_API_KEY'],
95
+ required: true,
96
+ },
97
+ {
98
+ id: 'mistral',
99
+ name: 'Mistral',
100
+ envKeys: ['MISTRAL_API_KEY'],
101
+ required: true,
102
+ },
103
+ {
104
+ id: 'togetherai',
105
+ name: 'Together.ai',
106
+ envKeys: ['TOGETHER_AI_API_KEY'],
107
+ required: true,
108
+ providerIds: ['together.ai'],
109
+ },
110
+ {
111
+ id: 'cohere',
112
+ name: 'Cohere',
113
+ envKeys: ['COHERE_API_KEY'],
114
+ required: true,
115
+ },
116
+ {
117
+ id: 'cerebras',
118
+ name: 'Cerebras',
119
+ envKeys: ['CEREBRAS_API_KEY'],
120
+ required: true,
121
+ },
122
+ {
123
+ id: 'perplexity',
124
+ name: 'Perplexity',
125
+ envKeys: ['PERPLEXITY_API_KEY'],
126
+ required: true,
127
+ },
128
+ {
129
+ id: 'openrouter',
130
+ name: 'OpenRouter',
131
+ envKeys: ['OPENROUTER_API_KEY'],
132
+ required: true,
133
+ providerIds: ['open-router'],
134
+ },
135
+ {
136
+ id: 'openai-compatible',
137
+ name: 'OpenAI Compatible',
138
+ envKeys: ['OPENAI_COMPATIBLE_API_KEY'],
139
+ required: true,
140
+ },
141
+ {
142
+ id: 'gitlab',
143
+ name: 'GitLab',
144
+ envKeys: ['GITLAB_TOKEN'],
145
+ required: true,
146
+ providerIds: ['gitlab-ai', 'gitlab-duo'],
147
+ },
148
+ ];
149
+
150
+ const ENV_KEY_ALIASES: Record<string, string[]> = {
151
+ GOOGLE_GENERATIVE_AI_API_KEY: ['GEMINI_API_KEY'],
152
+ };
153
+
154
+ const SECRET_SERVICE = 'zencommit';
155
+
156
+ export const getSecretLabel = (envKey: string): string => `${SECRET_SERVICE}:${envKey}`;
157
+
158
+ const toSecretOptions = (envKey: string): { service: string; name: string } => ({
159
+ service: SECRET_SERVICE,
160
+ name: envKey,
161
+ });
162
+
163
+ export const setSecret = async (envKey: string, value: string): Promise<void> => {
164
+ await Bun.secrets.set(toSecretOptions(envKey), value);
165
+ };
166
+
167
+ export const getSecret = async (envKey: string): Promise<string | null> => {
168
+ const value = await Bun.secrets.get(toSecretOptions(envKey));
169
+ if (!value) {
170
+ return null;
171
+ }
172
+ return value;
173
+ };
174
+
175
+ export const deleteSecret = async (envKey: string): Promise<void> => {
176
+ await Bun.secrets.delete(toSecretOptions(envKey));
177
+ };
178
+
179
+ const PROVIDER_AUTH_INDEX: Map<string, ProviderAuthConfig> = new Map(
180
+ PROVIDER_AUTH_CONFIGS.flatMap((config) => {
181
+ const aliases = config.providerIds ?? [];
182
+ return [config.id, ...aliases].map((id) => [id.toLowerCase(), config]);
183
+ }),
184
+ );
185
+
186
+ export const getProviderAuthConfigs = (): ProviderAuthConfig[] => [...PROVIDER_AUTH_CONFIGS];
187
+
188
+ export const resolveProviderAuth = (modelId: string): ProviderAuthConfig | null => {
189
+ const provider = modelId.split('/')[0];
190
+ if (!provider) {
191
+ return null;
192
+ }
193
+ return PROVIDER_AUTH_INDEX.get(provider.toLowerCase()) ?? null;
194
+ };
195
+
196
+ export const getKnownEnvKeys = (): string[] => {
197
+ const keys = new Set<string>();
198
+ for (const config of PROVIDER_AUTH_CONFIGS) {
199
+ for (const envKey of config.envKeys) {
200
+ keys.add(envKey);
201
+ }
202
+ }
203
+ return Array.from(keys).sort();
204
+ };
205
+
206
+ export const resolveRuntimeSecrets = async (envKeys: string[]): Promise<Record<string, string>> => {
207
+ const resolved: Record<string, string> = {};
208
+ for (const envKey of envKeys) {
209
+ const secret = await getSecret(envKey);
210
+ if (secret) {
211
+ resolved[envKey] = secret;
212
+ continue;
213
+ }
214
+ const envValue = process.env[envKey];
215
+ if (envValue) {
216
+ resolved[envKey] = envValue;
217
+ continue;
218
+ }
219
+ const aliases = ENV_KEY_ALIASES[envKey] ?? [];
220
+ for (const alias of aliases) {
221
+ const aliasSecret = await getSecret(alias);
222
+ if (aliasSecret) {
223
+ resolved[envKey] = aliasSecret;
224
+ break;
225
+ }
226
+ const aliasValue = process.env[alias];
227
+ if (aliasValue) {
228
+ resolved[envKey] = aliasValue;
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ return resolved;
234
+ };
@@ -0,0 +1,138 @@
1
+ import { generateText } from 'ai';
2
+ import { ConfigLoadError, resolveConfig } from '../config/load.js';
3
+ import { getRepoRoot } from '../git/repo.js';
4
+ import {
5
+ deleteSecret,
6
+ getKnownEnvKeys,
7
+ getSecret,
8
+ resolveProviderAuth,
9
+ setSecret,
10
+ } from '../auth/secrets.js';
11
+ import { promptForSecret, selectProviderKey } from '../ui/prompts.js';
12
+ import { redactValue } from '../util/redact.js';
13
+ import { logVerbose } from '../util/logger.js';
14
+ import { resolveLanguageModel } from '../llm/providers.js';
15
+
16
+ interface AuthArgs {
17
+ envKey?: string;
18
+ token?: string;
19
+ }
20
+
21
+ const validateEnvKey = (envKey: string): void => {
22
+ const supported = new Set(getKnownEnvKeys());
23
+ if (!supported.has(envKey)) {
24
+ throw new Error(`Unsupported env key: ${envKey}`);
25
+ }
26
+ };
27
+
28
+ const resolveModel = (modelId: string, openaiCompatible?: { baseUrl?: string; name?: string }) => {
29
+ try {
30
+ return resolveLanguageModel(modelId, { openaiCompatible });
31
+ } catch {
32
+ return null;
33
+ }
34
+ };
35
+
36
+ const verifyCredentials = async (
37
+ modelId: string,
38
+ timeoutMs = 5000,
39
+ openaiCompatible?: { baseUrl?: string; name?: string },
40
+ ): Promise<boolean> => {
41
+ const model = resolveModel(modelId, openaiCompatible);
42
+ if (!model) {
43
+ logVerbose(1, `auth verify: skipping unsupported model ${modelId}`);
44
+ return true;
45
+ }
46
+ try {
47
+ await Promise.race([
48
+ generateText({
49
+ model,
50
+ messages: [
51
+ { role: 'system', content: 'Reply with OK.' },
52
+ { role: 'user', content: 'OK' },
53
+ ],
54
+ maxTokens: 4,
55
+ temperature: 0,
56
+ }),
57
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs)),
58
+ ]);
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ };
64
+
65
+ export const runAuthLogin = async (args: AuthArgs): Promise<void> => {
66
+ const envKey = args.envKey ?? (await selectProviderKey());
67
+ if (!envKey) {
68
+ process.exit(0);
69
+ }
70
+ logVerbose(1, `auth login: env key ${envKey}`);
71
+ try {
72
+ validateEnvKey(envKey);
73
+ } catch (error) {
74
+ console.error((error as Error).message);
75
+ process.exit(2);
76
+ }
77
+
78
+ const token = args.token ?? (await promptForSecret(envKey));
79
+ if (!token) {
80
+ process.exit(0);
81
+ }
82
+ logVerbose(2, `auth login: storing secret for ${envKey}`);
83
+
84
+ await setSecret(envKey, token);
85
+ process.env[envKey] = token;
86
+
87
+ try {
88
+ const repoRoot = await getRepoRoot();
89
+ const config = await resolveConfig(repoRoot);
90
+ const auth = resolveProviderAuth(config.ai.model);
91
+ if (auth && auth.envKeys.includes(envKey)) {
92
+ logVerbose(2, `auth login: verifying model ${config.ai.model}`);
93
+ const ok = await verifyCredentials(config.ai.model, 5000, config.ai.openaiCompatible);
94
+ if (!ok) {
95
+ console.warn('Stored secret but verification failed. Check your key and network.');
96
+ }
97
+ }
98
+ } catch (error) {
99
+ if (error instanceof ConfigLoadError) {
100
+ console.warn(`Config load failed; skipping verification: ${error.message}`);
101
+ } else {
102
+ console.warn('Verification skipped due to unexpected error.');
103
+ }
104
+ }
105
+
106
+ console.log(`Stored ${envKey} in Bun secrets.`);
107
+ };
108
+
109
+ export const runAuthLogout = async (args: AuthArgs): Promise<void> => {
110
+ const envKey = args.envKey ?? (await selectProviderKey());
111
+ if (!envKey) {
112
+ process.exit(0);
113
+ }
114
+ logVerbose(1, `auth logout: env key ${envKey}`);
115
+ try {
116
+ validateEnvKey(envKey);
117
+ } catch (error) {
118
+ console.error((error as Error).message);
119
+ process.exit(2);
120
+ }
121
+ await deleteSecret(envKey);
122
+ console.log(`Removed ${envKey} from Bun secrets.`);
123
+ };
124
+
125
+ export const runAuthStatus = async (): Promise<void> => {
126
+ const keys = getKnownEnvKeys();
127
+ logVerbose(1, `auth status: checking ${keys.length} keys`);
128
+ for (const envKey of keys) {
129
+ const secret = await getSecret(envKey);
130
+ if (secret) {
131
+ console.log(`${envKey}: stored (${redactValue(secret)})`);
132
+ } else if (process.env[envKey]) {
133
+ console.log(`${envKey}: set in environment`);
134
+ } else {
135
+ console.log(`${envKey}: missing`);
136
+ }
137
+ }
138
+ };
@@ -0,0 +1,83 @@
1
+ import { defaultConfig } from '../config/types.js';
2
+ import { ConfigLoadError, resolveConfig, resolveConfigWithSources } from '../config/load.js';
3
+ import { validateConfig } from '../config/validate.js';
4
+ import { getRepoRoot } from '../git/repo.js';
5
+ import { fileExists, writeJsonFile } from '../util/fs.js';
6
+ import { redactObject } from '../util/redact.js';
7
+ import { logVerbose } from '../util/logger.js';
8
+ import path from 'node:path';
9
+
10
+ export const runConfigPrint = async (): Promise<void> => {
11
+ try {
12
+ const repoRoot = await getRepoRoot();
13
+ logVerbose(1, `config print: repo root ${repoRoot ?? 'unknown'}`);
14
+ const { config, sourceMap } = await resolveConfigWithSources(repoRoot);
15
+ const redacted = redactObject(config);
16
+ console.log(JSON.stringify(redacted, null, 2));
17
+ console.log('\nSources:');
18
+ for (const [key, source] of Object.entries(sourceMap)) {
19
+ console.log(`${key}: ${source}`);
20
+ }
21
+ } catch (error) {
22
+ if (error instanceof ConfigLoadError) {
23
+ console.error(error.message);
24
+ process.exit(2);
25
+ }
26
+ throw error;
27
+ }
28
+ };
29
+
30
+ export const runConfigInit = async (): Promise<void> => {
31
+ const repoRoot = await getRepoRoot();
32
+ if (!repoRoot) {
33
+ console.error('Not inside a git repository.');
34
+ process.exit(3);
35
+ }
36
+ logVerbose(1, `config init: repo root ${repoRoot}`);
37
+ const configPath = path.join(repoRoot, 'zencommit.json');
38
+ if (await fileExists(configPath)) {
39
+ console.error('zencommit.json already exists.');
40
+ process.exit(2);
41
+ }
42
+ await writeJsonFile(configPath, defaultConfig);
43
+ console.log(`Wrote ${configPath}`);
44
+ };
45
+
46
+ export const runConfigValidate = async (): Promise<void> => {
47
+ try {
48
+ const repoRoot = await getRepoRoot();
49
+ logVerbose(1, `config validate: repo root ${repoRoot ?? 'unknown'}`);
50
+ const config = await resolveConfig(repoRoot);
51
+ const result = validateConfig(config);
52
+ if (result.valid) {
53
+ console.log('Config is valid.');
54
+ return;
55
+ }
56
+ console.error('Config validation failed:');
57
+ for (const error of result.errors) {
58
+ console.error(`- ${error.path}: ${error.message}`);
59
+ }
60
+ process.exit(2);
61
+ } catch (error) {
62
+ if (error instanceof ConfigLoadError) {
63
+ console.error(error.message);
64
+ process.exit(2);
65
+ }
66
+ throw error;
67
+ }
68
+ };
69
+
70
+ export const runConfigShowResolved = async (): Promise<void> => {
71
+ try {
72
+ const repoRoot = await getRepoRoot();
73
+ const config = await resolveConfig(repoRoot);
74
+ const redacted = redactObject(config);
75
+ console.log(JSON.stringify(redacted, null, 2));
76
+ } catch (error) {
77
+ if (error instanceof ConfigLoadError) {
78
+ console.error(error.message);
79
+ process.exit(2);
80
+ }
81
+ throw error;
82
+ }
83
+ };