@freetison/git-super 0.2.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.
- package/LICENSE +201 -0
- package/README.md +384 -0
- package/bin/git-super.mjs +576 -0
- package/lib/ARCHITECTURE.md +254 -0
- package/lib/auth/auth-strategy.mjs +132 -0
- package/lib/auth/credential-store.mjs +222 -0
- package/lib/auth/oauth-flows.mjs +266 -0
- package/lib/auth/token-manager.mjs +246 -0
- package/lib/cli/auth-commands.mjs +327 -0
- package/lib/config/config-loader.mjs +167 -0
- package/lib/fallback/add-files-strategy.mjs +15 -0
- package/lib/fallback/base-fallback-strategy.mjs +34 -0
- package/lib/fallback/delete-files-strategy.mjs +15 -0
- package/lib/fallback/fallback-resolver.mjs +54 -0
- package/lib/fallback/modify-files-strategy.mjs +15 -0
- package/lib/providers/anthropic-provider.mjs +44 -0
- package/lib/providers/azure-openai-provider.mjs +185 -0
- package/lib/providers/base-oauth-provider.mjs +62 -0
- package/lib/providers/base-provider.mjs +29 -0
- package/lib/providers/generic-oidc-provider.mjs +144 -0
- package/lib/providers/github-copilot-provider.mjs +113 -0
- package/lib/providers/ollama-provider.mjs +109 -0
- package/lib/providers/openai-provider.mjs +44 -0
- package/lib/providers/provider-registry.mjs +99 -0
- package/package.json +59 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Authentication Commands
|
|
3
|
+
* Handles auth login, logout, status, and context management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { getConfigPath, listOrganizations, loadConfig } from '../config/config-loader.mjs';
|
|
8
|
+
import { ProviderRegistry } from '../providers/provider-registry.mjs';
|
|
9
|
+
import { openBrowser } from '../auth/oauth-flows.mjs';
|
|
10
|
+
|
|
11
|
+
const colors = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bright: '\x1b[1m',
|
|
14
|
+
green: '\x1b[32m',
|
|
15
|
+
yellow: '\x1b[33m',
|
|
16
|
+
blue: '\x1b[34m',
|
|
17
|
+
cyan: '\x1b[36m',
|
|
18
|
+
red: '\x1b[31m',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function log(message, color = 'reset') {
|
|
22
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Handle 'auth login' command
|
|
27
|
+
*/
|
|
28
|
+
export async function handleAuthLogin(args) {
|
|
29
|
+
const providerName = args.provider || args.p;
|
|
30
|
+
const orgId = args.org || args.o;
|
|
31
|
+
|
|
32
|
+
if (!providerName) {
|
|
33
|
+
log('Usage: git super auth login --provider <name> [--org <org-id>]', 'yellow');
|
|
34
|
+
log('\nAvailable OAuth providers:', 'bright');
|
|
35
|
+
log(' • github-copilot GitHub Copilot Enterprise', 'cyan');
|
|
36
|
+
log(' • azure-openai Azure OpenAI with Azure AD', 'cyan');
|
|
37
|
+
log(' • generic-oidc Generic OIDC provider', 'cyan');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Load config for the specified org (if provided)
|
|
43
|
+
let config = loadConfig();
|
|
44
|
+
|
|
45
|
+
if (orgId && config.organizations) {
|
|
46
|
+
const orgConfig = config.organizations[orgId];
|
|
47
|
+
if (!orgConfig) {
|
|
48
|
+
throw new Error(`Organization '${orgId}' not found in config`);
|
|
49
|
+
}
|
|
50
|
+
config = { ...config, ...orgConfig };
|
|
51
|
+
log(`\n🏢 Using organization: ${orgConfig.name || orgId}`, 'cyan');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get provider instance
|
|
55
|
+
const registry = new ProviderRegistry(config);
|
|
56
|
+
|
|
57
|
+
// Create provider if not in registry (force initialization)
|
|
58
|
+
let provider;
|
|
59
|
+
try {
|
|
60
|
+
provider = registry.get(providerName);
|
|
61
|
+
} catch {
|
|
62
|
+
// Provider not auto-registered, try to create it manually
|
|
63
|
+
const { GitHubCopilotProvider } = await import('../providers/github-copilot-provider.mjs');
|
|
64
|
+
const { AzureOpenAIProvider } = await import('../providers/azure-openai-provider.mjs');
|
|
65
|
+
const { GenericOIDCProvider } = await import('../providers/generic-oidc-provider.mjs');
|
|
66
|
+
|
|
67
|
+
switch (providerName) {
|
|
68
|
+
case 'github-copilot':
|
|
69
|
+
provider = new GitHubCopilotProvider(config);
|
|
70
|
+
break;
|
|
71
|
+
case 'azure-openai':
|
|
72
|
+
provider = new AzureOpenAIProvider(config);
|
|
73
|
+
break;
|
|
74
|
+
case 'generic-oidc':
|
|
75
|
+
provider = new GenericOIDCProvider(config);
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Provider '${providerName}' is not an OAuth provider or doesn't exist`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!provider.initiateAuth) {
|
|
83
|
+
throw new Error(`Provider '${providerName}' does not support OAuth authentication`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
log(`\n🔐 Initiating OAuth authentication for ${providerName}...`, 'bright');
|
|
87
|
+
|
|
88
|
+
// Step 1: Initiate device code flow
|
|
89
|
+
const deviceAuth = await provider.initiateAuth();
|
|
90
|
+
|
|
91
|
+
// Step 2: Display code to user
|
|
92
|
+
log(`\n📝 User Code: ${colors.bright}${deviceAuth.userCode}${colors.reset}`, 'green');
|
|
93
|
+
log(`\n🌐 Verification URL: ${colors.cyan}${deviceAuth.verificationUri}${colors.reset}`);
|
|
94
|
+
|
|
95
|
+
// Try to open browser
|
|
96
|
+
const opened = await openBrowser(deviceAuth.verificationUriComplete || deviceAuth.verificationUri);
|
|
97
|
+
|
|
98
|
+
if (opened) {
|
|
99
|
+
log('\n✅ Browser opened. Please complete authentication in your browser.', 'green');
|
|
100
|
+
} else {
|
|
101
|
+
log('\n⚠️ Could not open browser automatically. Please open the URL above manually.', 'yellow');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
log(`\n⏳ Waiting for authorization (expires in ${Math.floor(deviceAuth.expiresIn / 60)} minutes)...`, 'cyan');
|
|
105
|
+
|
|
106
|
+
// Step 3: Poll for token
|
|
107
|
+
await provider.completeAuth(deviceAuth.deviceCode, deviceAuth.interval);
|
|
108
|
+
|
|
109
|
+
log('\n✅ Authentication successful!', 'green');
|
|
110
|
+
|
|
111
|
+
// Show token info
|
|
112
|
+
const tokenInfo = await provider.tokenManager.getTokenInfo();
|
|
113
|
+
if (tokenInfo?.expiresAt) {
|
|
114
|
+
const expiresAt = new Date(tokenInfo.expiresAt);
|
|
115
|
+
log(` Token valid until: ${expiresAt.toLocaleString()}`, 'cyan');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
log(`\n💡 You can now use: git super`, 'bright');
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
log(`\n❌ Authentication failed: ${error.message}`, 'red');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Handle 'auth logout' command
|
|
128
|
+
*/
|
|
129
|
+
export async function handleAuthLogout(args) {
|
|
130
|
+
const providerName = args.provider || args.p;
|
|
131
|
+
const all = args.all || args.a;
|
|
132
|
+
|
|
133
|
+
const config = loadConfig();
|
|
134
|
+
const registry = new ProviderRegistry(config);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
if (all) {
|
|
138
|
+
// Logout from all OAuth providers
|
|
139
|
+
log('\n🔓 Logging out from all providers...', 'yellow');
|
|
140
|
+
|
|
141
|
+
for (const [name, provider] of registry.providers) {
|
|
142
|
+
if (provider.tokenManager) {
|
|
143
|
+
try {
|
|
144
|
+
await provider.tokenManager.revokeToken();
|
|
145
|
+
log(` ✅ ${name}`, 'green');
|
|
146
|
+
} catch (error) {
|
|
147
|
+
log(` ⚠️ ${name}: ${error.message}`, 'yellow');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
log('\n✅ Logged out from all providers', 'green');
|
|
153
|
+
} else if (providerName) {
|
|
154
|
+
// Logout from specific provider
|
|
155
|
+
const provider = registry.get(providerName);
|
|
156
|
+
|
|
157
|
+
if (!provider.tokenManager) {
|
|
158
|
+
throw new Error(`Provider '${providerName}' does not use OAuth authentication`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
log(`\n🔓 Logging out from ${providerName}...`, 'yellow');
|
|
162
|
+
await provider.tokenManager.revokeToken();
|
|
163
|
+
log('✅ Successfully logged out', 'green');
|
|
164
|
+
} else {
|
|
165
|
+
// Logout from current provider
|
|
166
|
+
const currentProvider = registry.get(config.aiProvider);
|
|
167
|
+
|
|
168
|
+
if (!currentProvider.tokenManager) {
|
|
169
|
+
throw new Error(`Current provider '${config.aiProvider}' does not use OAuth authentication`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
log(`\n🔓 Logging out from ${config.aiProvider}...`, 'yellow');
|
|
173
|
+
await currentProvider.tokenManager.revokeToken();
|
|
174
|
+
log('✅ Successfully logged out', 'green');
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
log(`\n❌ Logout failed: ${error.message}`, 'red');
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle 'auth status' command
|
|
184
|
+
*/
|
|
185
|
+
export async function handleAuthStatus() {
|
|
186
|
+
const config = loadConfig();
|
|
187
|
+
const registry = new ProviderRegistry(config);
|
|
188
|
+
|
|
189
|
+
log('\n📊 Authentication Status\n', 'bright');
|
|
190
|
+
|
|
191
|
+
// Show active context
|
|
192
|
+
if (config.activeOrg) {
|
|
193
|
+
const orgConfig = config.organizations?.[config.activeOrg];
|
|
194
|
+
log(`Active Context: ${colors.cyan}${orgConfig?.name || config.activeOrg}${colors.reset}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
log(`Current Provider: ${colors.cyan}${config.aiProvider}${colors.reset}`);
|
|
198
|
+
log(`Model: ${colors.cyan}${config.aiModel}${colors.reset}\n`);
|
|
199
|
+
|
|
200
|
+
// Check each OAuth provider
|
|
201
|
+
for (const [name, provider] of registry.providers) {
|
|
202
|
+
if (provider.tokenManager) {
|
|
203
|
+
const tokenInfo = await provider.tokenManager.getTokenInfo();
|
|
204
|
+
|
|
205
|
+
if (tokenInfo?.hasToken) {
|
|
206
|
+
const status = tokenInfo.isValid ? '✅' : '❌';
|
|
207
|
+
const statusText = tokenInfo.isValid ? 'Valid' : 'Expired';
|
|
208
|
+
const expiresAt = tokenInfo.expiresAt ? new Date(tokenInfo.expiresAt).toLocaleString() : 'N/A';
|
|
209
|
+
|
|
210
|
+
log(`${status} ${colors.bright}${name}${colors.reset}: ${statusText}`, tokenInfo.isValid ? 'green' : 'red');
|
|
211
|
+
log(` Expires: ${expiresAt}`, 'cyan');
|
|
212
|
+
} else {
|
|
213
|
+
log(`⚪ ${colors.bright}${name}${colors.reset}: Not authenticated`, 'yellow');
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// API key provider
|
|
217
|
+
if (name === config.aiProvider) {
|
|
218
|
+
const hasKey = await provider.authStrategy?.isValid();
|
|
219
|
+
const status = hasKey ? '✅' : '❌';
|
|
220
|
+
const statusText = hasKey ? 'API key configured' : 'No API key';
|
|
221
|
+
log(`${status} ${colors.bright}${name}${colors.reset}: ${statusText}`, hasKey ? 'green' : 'red');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Handle 'context' commands
|
|
231
|
+
*/
|
|
232
|
+
export async function handleContext(args) {
|
|
233
|
+
const subcommand = args._[1]; // 'list', 'switch', 'create'
|
|
234
|
+
const config = loadConfig();
|
|
235
|
+
|
|
236
|
+
switch (subcommand) {
|
|
237
|
+
case 'list':
|
|
238
|
+
case 'ls':
|
|
239
|
+
handleContextList();
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case 'switch':
|
|
243
|
+
case 'sw':
|
|
244
|
+
handleContextSwitch(args);
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case 'create':
|
|
248
|
+
log('Context creation wizard not yet implemented', 'yellow');
|
|
249
|
+
log('For now, edit ~/.gitsuperrc manually to add new contexts', 'cyan');
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
default:
|
|
253
|
+
// No subcommand: show current context
|
|
254
|
+
handleContextCurrent();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function handleContextList() {
|
|
259
|
+
const orgs = listOrganizations();
|
|
260
|
+
|
|
261
|
+
if (orgs.length === 0) {
|
|
262
|
+
log('\n⚠️ No organizations configured', 'yellow');
|
|
263
|
+
log('Add organizations to ~/.gitsuperrc to use multi-context mode\n', 'cyan');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
log('\n📋 Available Contexts:\n', 'bright');
|
|
268
|
+
|
|
269
|
+
for (const org of orgs) {
|
|
270
|
+
const marker = org.isActive ? '* ' : ' ';
|
|
271
|
+
const color = org.isActive ? 'green' : 'cyan';
|
|
272
|
+
log(`${marker}${colors[color]}${org.id}${colors.reset} (${org.name})`, color);
|
|
273
|
+
log(` Provider: ${org.aiProvider}/${org.aiModel}`, 'cyan');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function handleContextCurrent() {
|
|
280
|
+
const config = loadConfig();
|
|
281
|
+
|
|
282
|
+
log('\n📍 Current Context\n', 'bright');
|
|
283
|
+
|
|
284
|
+
if (config.activeOrg) {
|
|
285
|
+
const orgConfig = config.organizations?.[config.activeOrg];
|
|
286
|
+
log(`Organization: ${colors.cyan}${orgConfig?.name || config.activeOrg}${colors.reset}`);
|
|
287
|
+
} else {
|
|
288
|
+
log('Mode: Legacy (no organizations)', 'yellow');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
log(`Provider: ${colors.cyan}${config.aiProvider}${colors.reset}`);
|
|
292
|
+
log(`Model: ${colors.cyan}${config.aiModel}${colors.reset}`);
|
|
293
|
+
|
|
294
|
+
console.log();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function handleContextSwitch(args) {
|
|
298
|
+
const targetOrg = args._[2] || args.org || args.o;
|
|
299
|
+
|
|
300
|
+
if (!targetOrg) {
|
|
301
|
+
log('Usage: git super context switch <org-id>', 'yellow');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const configPath = getConfigPath();
|
|
307
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
308
|
+
const config = JSON.parse(content);
|
|
309
|
+
|
|
310
|
+
if (!config.organizations || !config.organizations[targetOrg]) {
|
|
311
|
+
throw new Error(`Organization '${targetOrg}' not found`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Update active org
|
|
315
|
+
config.activeOrg = targetOrg;
|
|
316
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
317
|
+
|
|
318
|
+
const orgConfig = config.organizations[targetOrg];
|
|
319
|
+
log(`\n✅ Switched to: ${colors.green}${orgConfig.name || targetOrg}${colors.reset}`, 'green');
|
|
320
|
+
log(` Provider: ${orgConfig.aiProvider}/${orgConfig.aiModel}`, 'cyan');
|
|
321
|
+
console.log();
|
|
322
|
+
|
|
323
|
+
} catch (error) {
|
|
324
|
+
log(`\n❌ Failed to switch context: ${error.message}`, 'red');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader with layered approach (no if-else chains)
|
|
3
|
+
* Layers: defaults → file config → environment variables
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Map environment variables to config keys
|
|
12
|
+
*/
|
|
13
|
+
const ENV_MAPPINGS = {
|
|
14
|
+
aiProvider: 'AI_PROVIDER',
|
|
15
|
+
aiModel: 'AI_MODEL',
|
|
16
|
+
ollamaUrl: 'OLLAMA_URL',
|
|
17
|
+
anthropicKey: 'ANTHROPIC_API_KEY',
|
|
18
|
+
openaiKey: 'OPENAI_API_KEY',
|
|
19
|
+
|
|
20
|
+
// OAuth / Enterprise configs
|
|
21
|
+
githubOrg: 'GITHUB_ORG',
|
|
22
|
+
githubClientId: 'GITHUB_CLIENT_ID',
|
|
23
|
+
azureTenantId: 'AZURE_TENANT_ID',
|
|
24
|
+
azureClientId: 'AZURE_CLIENT_ID',
|
|
25
|
+
azureResourceEndpoint: 'AZURE_OPENAI_ENDPOINT',
|
|
26
|
+
oidcIssuer: 'OIDC_ISSUER',
|
|
27
|
+
oidcClientId: 'OIDC_CLIENT_ID',
|
|
28
|
+
oidcApiEndpoint: 'OIDC_API_ENDPOINT',
|
|
29
|
+
|
|
30
|
+
// Active organization context
|
|
31
|
+
activeOrg: 'GIT_SUPER_ACTIVE_ORG',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Default configuration values
|
|
36
|
+
*/
|
|
37
|
+
function getDefaults() {
|
|
38
|
+
return {
|
|
39
|
+
aiProvider: 'ollama',
|
|
40
|
+
aiModel: 'mistral:latest',
|
|
41
|
+
ollamaUrl: 'http://localhost:11434',
|
|
42
|
+
messageTemplate: null,
|
|
43
|
+
ticketNumber: '',
|
|
44
|
+
commitRules: {
|
|
45
|
+
types: ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'ci', 'build'],
|
|
46
|
+
maxLength: 72,
|
|
47
|
+
allowEmptyScope: true
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load configuration from file
|
|
54
|
+
*/
|
|
55
|
+
function loadFileConfig(configPath) {
|
|
56
|
+
if (!existsSync(configPath)) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
62
|
+
return JSON.parse(content);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn(`⚠️ Error reading config file: ${error.message}`);
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply environment variable overrides using object mapping
|
|
71
|
+
*/
|
|
72
|
+
function applyEnvOverrides(config) {
|
|
73
|
+
const overrides = {};
|
|
74
|
+
|
|
75
|
+
Object.entries(ENV_MAPPINGS).forEach(([key, envVar]) => {
|
|
76
|
+
const value = process.env[envVar];
|
|
77
|
+
if (value !== undefined) {
|
|
78
|
+
overrides[key] = value;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { ...config, ...overrides };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Load configuration with layered approach
|
|
87
|
+
* Priority: ENV vars > file config > defaults
|
|
88
|
+
*
|
|
89
|
+
* Supports two modes:
|
|
90
|
+
* 1. Legacy mode: flat config (backward compatible)
|
|
91
|
+
* 2. Multi-org mode: organizations with active context
|
|
92
|
+
*/
|
|
93
|
+
export function loadConfig() {
|
|
94
|
+
const configPath = join(homedir(), '.gitsuperrc');
|
|
95
|
+
|
|
96
|
+
// Layer 1: Defaults
|
|
97
|
+
let config = getDefaults();
|
|
98
|
+
|
|
99
|
+
// Layer 2: File config
|
|
100
|
+
const fileConfig = loadFileConfig(configPath);
|
|
101
|
+
|
|
102
|
+
// Check if using multi-org configuration
|
|
103
|
+
if (fileConfig.organizations) {
|
|
104
|
+
// Multi-org mode: load active organization context
|
|
105
|
+
const activeOrg = fileConfig.activeOrg || Object.keys(fileConfig.organizations)[0];
|
|
106
|
+
const orgConfig = fileConfig.organizations[activeOrg];
|
|
107
|
+
|
|
108
|
+
if (orgConfig) {
|
|
109
|
+
// Merge: defaults <- org config <- global fallbacks
|
|
110
|
+
config = {
|
|
111
|
+
...config,
|
|
112
|
+
...orgConfig,
|
|
113
|
+
activeOrg,
|
|
114
|
+
organizations: fileConfig.organizations, // Keep for context switching
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
console.warn(`⚠️ Active organization '${activeOrg}' not found in config, using defaults`);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// Legacy mode: flat config (backward compatible)
|
|
121
|
+
config = { ...config, ...fileConfig };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Layer 3: Environment variables (highest priority)
|
|
125
|
+
config = applyEnvOverrides(config);
|
|
126
|
+
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get configuration path
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
export function getConfigPath() {
|
|
135
|
+
return join(homedir(), '.gitsuperrc');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* List all available organizations from config
|
|
140
|
+
* @returns {Array<Object>} Array of { id, name, aiProvider, aiModel }
|
|
141
|
+
*/
|
|
142
|
+
export function listOrganizations() {
|
|
143
|
+
const configPath = getConfigPath();
|
|
144
|
+
const fileConfig = loadFileConfig(configPath);
|
|
145
|
+
|
|
146
|
+
if (!fileConfig.organizations) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return Object.entries(fileConfig.organizations).map(([id, org]) => ({
|
|
151
|
+
id,
|
|
152
|
+
name: org.name || id,
|
|
153
|
+
aiProvider: org.aiProvider || 'ollama',
|
|
154
|
+
aiModel: org.aiModel || 'unknown',
|
|
155
|
+
isActive: id === fileConfig.activeOrg,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get active organization ID
|
|
161
|
+
* @returns {string|null}
|
|
162
|
+
*/
|
|
163
|
+
export function getActiveOrg() {
|
|
164
|
+
const configPath = getConfigPath();
|
|
165
|
+
const fileConfig = loadFileConfig(configPath);
|
|
166
|
+
return fileConfig.activeOrg || null;
|
|
167
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Files Strategy - Handles when only new files are added
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BaseFallbackStrategy } from './base-fallback-strategy.mjs';
|
|
6
|
+
|
|
7
|
+
export class AddFilesStrategy extends BaseFallbackStrategy {
|
|
8
|
+
canHandle({ added, modified, deleted }) {
|
|
9
|
+
return added > 0 && modified === 0 && deleted === 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getMessage() {
|
|
13
|
+
return 'feat: add new files';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Fallback Strategy - Strategy Pattern interface
|
|
3
|
+
* All fallback strategies must extend this class
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class BaseFallbackStrategy {
|
|
7
|
+
/**
|
|
8
|
+
* Check if this strategy can handle the given file stats
|
|
9
|
+
* @param {Object} stats - File change statistics
|
|
10
|
+
* @param {number} stats.added - Number of added files
|
|
11
|
+
* @param {number} stats.modified - Number of modified files
|
|
12
|
+
* @param {number} stats.deleted - Number of deleted files
|
|
13
|
+
* @returns {boolean}
|
|
14
|
+
*/
|
|
15
|
+
canHandle(stats) {
|
|
16
|
+
throw new Error(`${this.constructor.name} must implement canHandle(stats)`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the fallback commit message
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
getMessage() {
|
|
24
|
+
throw new Error(`${this.constructor.name} must implement getMessage()`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get strategy name (for debugging)
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
getName() {
|
|
32
|
+
return this.constructor.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete Files Strategy - Handles when files are deleted
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BaseFallbackStrategy } from './base-fallback-strategy.mjs';
|
|
6
|
+
|
|
7
|
+
export class DeleteFilesStrategy extends BaseFallbackStrategy {
|
|
8
|
+
canHandle({ deleted }) {
|
|
9
|
+
return deleted > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getMessage() {
|
|
13
|
+
return 'chore: remove files';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback Resolver - Coordinates fallback strategies
|
|
3
|
+
* Uses Strategy Pattern to select appropriate message
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AddFilesStrategy } from './add-files-strategy.mjs';
|
|
7
|
+
import { ModifyFilesStrategy } from './modify-files-strategy.mjs';
|
|
8
|
+
import { DeleteFilesStrategy } from './delete-files-strategy.mjs';
|
|
9
|
+
|
|
10
|
+
export class FallbackResolver {
|
|
11
|
+
constructor() {
|
|
12
|
+
// Order matters: more specific strategies first
|
|
13
|
+
this.strategies = [
|
|
14
|
+
new AddFilesStrategy(),
|
|
15
|
+
new DeleteFilesStrategy(),
|
|
16
|
+
new ModifyFilesStrategy(),
|
|
17
|
+
];
|
|
18
|
+
this.defaultMessage = 'chore: update';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the appropriate fallback message based on file stats
|
|
23
|
+
* @param {Object} stats - File change statistics
|
|
24
|
+
* @param {number} stats.added - Number of added files
|
|
25
|
+
* @param {number} stats.modified - Number of modified files
|
|
26
|
+
* @param {number} stats.deleted - Number of deleted files
|
|
27
|
+
* @returns {string} - The fallback commit message
|
|
28
|
+
*/
|
|
29
|
+
resolve(stats) {
|
|
30
|
+
for (const strategy of this.strategies) {
|
|
31
|
+
if (strategy.canHandle(stats)) {
|
|
32
|
+
return strategy.getMessage();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.defaultMessage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add a custom strategy
|
|
41
|
+
* @param {BaseFallbackStrategy} strategy - Strategy to add
|
|
42
|
+
*/
|
|
43
|
+
addStrategy(strategy) {
|
|
44
|
+
this.strategies.push(strategy);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set default message for when no strategy matches
|
|
49
|
+
* @param {string} message - Default message
|
|
50
|
+
*/
|
|
51
|
+
setDefaultMessage(message) {
|
|
52
|
+
this.defaultMessage = message;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modify Files Strategy - Handles when files are modified
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BaseFallbackStrategy } from './base-fallback-strategy.mjs';
|
|
6
|
+
|
|
7
|
+
export class ModifyFilesStrategy extends BaseFallbackStrategy {
|
|
8
|
+
canHandle({ modified }) {
|
|
9
|
+
return modified > 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getMessage() {
|
|
13
|
+
return 'refactor: update code';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic AI Provider implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { BaseAIProvider } from './base-provider.mjs';
|
|
6
|
+
import { ApiKeyAuthStrategy } from '../auth/auth-strategy.mjs';
|
|
7
|
+
|
|
8
|
+
export class AnthropicProvider extends BaseAIProvider {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
// Use API Key authentication strategy
|
|
11
|
+
const authStrategy = new ApiKeyAuthStrategy(config, {
|
|
12
|
+
keyName: 'anthropicKey',
|
|
13
|
+
headerName: 'x-api-key',
|
|
14
|
+
headerFormat: '{key}',
|
|
15
|
+
});
|
|
16
|
+
super(config, authStrategy);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async generate(prompt) {
|
|
20
|
+
// Get auth headers from strategy
|
|
21
|
+
const authHeaders = await this.authStrategy.getAuthHeaders();
|
|
22
|
+
|
|
23
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...authHeaders,
|
|
28
|
+
'anthropic-version': '2023-06-01',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
model: this.config.aiModel || 'claude-sonnet-4-20250514',
|
|
32
|
+
max_tokens: 200,
|
|
33
|
+
messages: [{ role: 'user', content: prompt }],
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
throw new Error(`Anthropic error: ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const data = await response.json();
|
|
42
|
+
return data.content[0].text.trim().replace(/^["']|["']$/g, '');
|
|
43
|
+
}
|
|
44
|
+
}
|