@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.
@@ -0,0 +1,254 @@
1
+ # git-super - Architecture Documentation
2
+
3
+ ## 📐 Design Patterns Applied
4
+
5
+ This refactor eliminates all `if-else` chains by applying **SOLID principles** and **Gang of Four design patterns**.
6
+
7
+ ---
8
+
9
+ ## 🔧 Config Loader (Object Mapping)
10
+
11
+ **Pattern:** Layered Configuration
12
+ **Location:** `lib/config/config-loader.mjs`
13
+
14
+ ### Problem
15
+ ```javascript
16
+ // ❌ Before: if-else chain
17
+ if (process.env.AI_PROVIDER) defaults.aiProvider = process.env.AI_PROVIDER;
18
+ if (process.env.AI_MODEL) defaults.aiModel = process.env.AI_MODEL;
19
+ if (process.env.OLLAMA_URL) defaults.ollamaUrl = process.env.OLLAMA_URL;
20
+ // ... more if statements
21
+ ```
22
+
23
+ ### Solution
24
+ ```javascript
25
+ // ✅ After: Object mapping
26
+ const ENV_MAPPINGS = {
27
+ aiProvider: 'AI_PROVIDER',
28
+ aiModel: 'AI_MODEL',
29
+ ollamaUrl: 'OLLAMA_URL',
30
+ };
31
+
32
+ Object.entries(ENV_MAPPINGS).forEach(([key, envVar]) => {
33
+ if (process.env[envVar]) config[key] = process.env[envVar];
34
+ });
35
+ ```
36
+
37
+ **Benefits:**
38
+ - ✅ Add new env vars by just adding to mapping
39
+ - ✅ No more repetitive if-else
40
+ - ✅ Single source of truth
41
+
42
+ ---
43
+
44
+ ## 🤖 AI Providers (Strategy Pattern)
45
+
46
+ **Pattern:** Strategy + Registry/Factory
47
+ **Location:** `lib/providers/`
48
+
49
+ ### Structure
50
+ ```
51
+ lib/providers/
52
+ ├── base-provider.mjs # Abstract base class
53
+ ├── ollama-provider.mjs # Ollama implementation
54
+ ├── anthropic-provider.mjs # Anthropic implementation
55
+ ├── openai-provider.mjs # OpenAI implementation
56
+ └── provider-registry.mjs # Factory/Registry
57
+ ```
58
+
59
+ ### Problem
60
+ ```javascript
61
+ // ❌ Before: nested if-else
62
+ if (CONFIG.aiProvider === 'ollama') {
63
+ message = await callOllama(prompt);
64
+ } else if (CONFIG.aiProvider === 'anthropic') {
65
+ message = await callAnthropic(prompt);
66
+ } else if (CONFIG.aiProvider === 'openai') {
67
+ message = await callOpenAI(prompt);
68
+ } else {
69
+ throw new Error(`Unsupported provider: ${CONFIG.aiProvider}`);
70
+ }
71
+ ```
72
+
73
+ ### Solution
74
+ ```javascript
75
+ // ✅ After: Strategy Pattern
76
+ const provider = providerRegistry.get(CONFIG.aiProvider);
77
+ const message = await provider.generate(prompt);
78
+ ```
79
+
80
+ **Benefits:**
81
+ - ✅ Add new providers without modifying main code
82
+ - ✅ Each provider is self-contained
83
+ - ✅ Easy to test in isolation
84
+ - ✅ Follows Open/Closed Principle
85
+
86
+ ### Adding a New Provider
87
+
88
+ ```javascript
89
+ // 1. Create new provider
90
+ export class GroqProvider extends BaseAIProvider {
91
+ async generate(prompt) {
92
+ // Implementation
93
+ }
94
+ }
95
+
96
+ // 2. Register in provider-registry.mjs
97
+ this.register('groq', new GroqProvider(this.config));
98
+
99
+ // 3. Done! No changes to main code
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 🔄 Fallback Messages (Strategy Pattern)
105
+
106
+ **Pattern:** Strategy + Resolver
107
+ **Location:** `lib/fallback/`
108
+
109
+ ### Structure
110
+ ```
111
+ lib/fallback/
112
+ ├── base-fallback-strategy.mjs # Abstract base
113
+ ├── add-files-strategy.mjs # feat: add new files
114
+ ├── modify-files-strategy.mjs # refactor: update code
115
+ ├── delete-files-strategy.mjs # chore: remove files
116
+ └── fallback-resolver.mjs # Strategy coordinator
117
+ ```
118
+
119
+ ### Problem
120
+ ```javascript
121
+ // ❌ Before: nested if-else
122
+ let fallback = 'chore: update';
123
+ if (added > 0 && modified === 0 && deleted === 0) {
124
+ fallback = 'feat: add new files';
125
+ } else if (modified > 0) {
126
+ fallback = 'refactor: update code';
127
+ } else if (deleted > 0) {
128
+ fallback = 'chore: remove files';
129
+ }
130
+ ```
131
+
132
+ ### Solution
133
+ ```javascript
134
+ // ✅ After: Strategy Pattern
135
+ const stats = { added, modified, deleted };
136
+ const fallback = fallbackResolver.resolve(stats);
137
+ ```
138
+
139
+ **Benefits:**
140
+ - ✅ Each strategy is a separate class
141
+ - ✅ Easy to add new fallback rules
142
+ - ✅ Testable in isolation
143
+ - ✅ Clear separation of concerns
144
+
145
+ ### Adding a New Fallback Strategy
146
+
147
+ ```javascript
148
+ // 1. Create strategy
149
+ export class RenameFilesStrategy extends BaseFallbackStrategy {
150
+ canHandle({ renamed }) {
151
+ return renamed > 0;
152
+ }
153
+
154
+ getMessage() {
155
+ return 'refactor: rename files';
156
+ }
157
+ }
158
+
159
+ // 2. Register in fallback-resolver.mjs
160
+ this.strategies = [
161
+ new AddFilesStrategy(),
162
+ new RenameFilesStrategy(), // Add here
163
+ // ...
164
+ ];
165
+
166
+ // 3. Done!
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 📊 Before vs After
172
+
173
+ ### Lines of Code
174
+ - **Before:** 643 lines (monolithic)
175
+ - **After:** ~450 lines (modular)
176
+ - **Reduction:** 30% less code in main file
177
+
178
+ ### Cyclomatic Complexity
179
+ - **Before:** High (nested if-else chains)
180
+ - **After:** Low (delegated to strategies)
181
+
182
+ ### Testability
183
+ - **Before:** Hard to test (everything coupled)
184
+ - **After:** Easy to test (each module isolated)
185
+
186
+ ### Extensibility
187
+ - **Before:** Modify main file for every addition
188
+ - **After:** Just add new strategy/provider class
189
+
190
+ ---
191
+
192
+ ## 🎯 SOLID Principles Applied
193
+
194
+ 1. **Single Responsibility Principle (SRP)**
195
+ - Each provider handles ONE AI service
196
+ - Each strategy handles ONE fallback scenario
197
+
198
+ 2. **Open/Closed Principle (OCP)**
199
+ - Open for extension (add new providers/strategies)
200
+ - Closed for modification (no changes to main code)
201
+
202
+ 3. **Liskov Substitution Principle (LSP)**
203
+ - All providers implement `BaseAIProvider`
204
+ - All can be used interchangeably
205
+
206
+ 4. **Dependency Inversion Principle (DIP)**
207
+ - Main code depends on abstractions (base classes)
208
+ - Not on concrete implementations
209
+
210
+ ---
211
+
212
+ ## 🧪 Testing Strategy
213
+
214
+ Each module can now be tested independently:
215
+
216
+ ```javascript
217
+ // Test config loader
218
+ import { loadConfig } from './lib/config/config-loader.mjs';
219
+ process.env.AI_PROVIDER = 'test';
220
+ const config = loadConfig();
221
+ assert(config.aiProvider === 'test');
222
+
223
+ // Test provider
224
+ import { OllamaProvider } from './lib/providers/ollama-provider.mjs';
225
+ const provider = new OllamaProvider(config);
226
+ const message = await provider.generate('test prompt');
227
+
228
+ // Test fallback
229
+ import { AddFilesStrategy } from './lib/fallback/add-files-strategy.mjs';
230
+ const strategy = new AddFilesStrategy();
231
+ assert(strategy.canHandle({ added: 1, modified: 0, deleted: 0 }));
232
+ ```
233
+
234
+ ---
235
+
236
+ ## 🚀 Performance
237
+
238
+ - **No performance penalty:** Patterns add negligible overhead
239
+ - **Better memory:** Lazy loading possible
240
+ - **Better maintainability:** Worth any minimal cost
241
+
242
+ ---
243
+
244
+ ## 📝 Code Review Checklist
245
+
246
+ ✅ No `if-else` chains
247
+ ✅ Each class has single responsibility
248
+ ✅ Easy to add new features
249
+ ✅ All modules testable in isolation
250
+ ✅ Clear separation of concerns
251
+ ✅ Follows Gang of Four patterns
252
+ ✅ C++/C# style OOP (as requested)
253
+
254
+ **Status:** Ready for PR ✅
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Authentication Strategy Pattern
3
+ * Defines how different providers authenticate (API Keys vs OAuth)
4
+ */
5
+
6
+ /**
7
+ * Base authentication strategy (abstract)
8
+ */
9
+ export class BaseAuthStrategy {
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+
14
+ /**
15
+ * Get authentication headers for API requests
16
+ * @returns {Promise<Object>} Headers object
17
+ */
18
+ async getAuthHeaders() {
19
+ throw new Error('Method getAuthHeaders() must be implemented by subclass');
20
+ }
21
+
22
+ /**
23
+ * Check if authentication is valid
24
+ * @returns {Promise<boolean>}
25
+ */
26
+ async isValid() {
27
+ throw new Error('Method isValid() must be implemented by subclass');
28
+ }
29
+
30
+ /**
31
+ * Get strategy name for logging
32
+ * @returns {string}
33
+ */
34
+ getName() {
35
+ return this.constructor.name;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * API Key Authentication Strategy
41
+ * Used by Anthropic, OpenAI (traditional API keys)
42
+ */
43
+ export class ApiKeyAuthStrategy extends BaseAuthStrategy {
44
+ constructor(config, options = {}) {
45
+ super(config);
46
+ this.keyName = options.keyName; // e.g., 'anthropicKey', 'openaiKey'
47
+ this.headerName = options.headerName; // e.g., 'x-api-key', 'Authorization'
48
+ this.headerFormat = options.headerFormat; // e.g., 'Bearer {key}', '{key}'
49
+ }
50
+
51
+ async getAuthHeaders() {
52
+ const apiKey = this.config[this.keyName];
53
+
54
+ if (!apiKey) {
55
+ throw new Error(`${this.keyName} is not configured. Please set it via environment variable or config file.`);
56
+ }
57
+
58
+ const value = this.headerFormat
59
+ ? this.headerFormat.replace('{key}', apiKey)
60
+ : apiKey;
61
+
62
+ return {
63
+ [this.headerName]: value,
64
+ };
65
+ }
66
+
67
+ async isValid() {
68
+ return !!this.config[this.keyName];
69
+ }
70
+ }
71
+
72
+ /**
73
+ * OAuth Authentication Strategy
74
+ * Used by GitHub Copilot, Azure OpenAI, Claude Enterprise
75
+ */
76
+ export class OAuthAuthStrategy extends BaseAuthStrategy {
77
+ constructor(config, tokenManager) {
78
+ super(config);
79
+ this.tokenManager = tokenManager;
80
+ }
81
+
82
+ async getAuthHeaders() {
83
+ // Ensure we have a valid token (refresh if needed)
84
+ await this.ensureAuthenticated();
85
+
86
+ const token = await this.tokenManager.getAccessToken();
87
+
88
+ if (!token) {
89
+ throw new Error('No valid OAuth token available. Please authenticate with: git super auth login');
90
+ }
91
+
92
+ return {
93
+ 'Authorization': `Bearer ${token}`,
94
+ };
95
+ }
96
+
97
+ async isValid() {
98
+ return await this.tokenManager.hasValidToken();
99
+ }
100
+
101
+ /**
102
+ * Ensure we have a valid token, refresh if needed
103
+ * @private
104
+ */
105
+ async ensureAuthenticated() {
106
+ if (!await this.tokenManager.hasValidToken()) {
107
+ // Try to refresh
108
+ const refreshed = await this.tokenManager.refreshToken();
109
+
110
+ if (!refreshed) {
111
+ throw new Error(
112
+ 'OAuth token expired and could not be refreshed. ' +
113
+ 'Please re-authenticate with: git super auth login'
114
+ );
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * No Auth Strategy
122
+ * Used by Ollama (local server, no authentication)
123
+ */
124
+ export class NoAuthStrategy extends BaseAuthStrategy {
125
+ async getAuthHeaders() {
126
+ return {}; // No authentication headers needed
127
+ }
128
+
129
+ async isValid() {
130
+ return true; // Always valid (local server)
131
+ }
132
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Secure Credential Storage
3
+ * Stores OAuth tokens and sensitive data using OS keychain or encrypted file
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { homedir } from 'node:os';
9
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from 'node:crypto';
10
+
11
+ /**
12
+ * Credential store with keychain fallback to encrypted file
13
+ */
14
+ export class CredentialStore {
15
+ constructor() {
16
+ this.keytarAvailable = this._checkKeytar();
17
+ this.storageDir = join(homedir(), '.gitsuper');
18
+ this.storageFile = join(this.storageDir, 'credentials.enc');
19
+
20
+ // Ensure storage directory exists
21
+ if (!existsSync(this.storageDir)) {
22
+ mkdirSync(this.storageDir, { recursive: true, mode: 0o700 });
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Check if keytar is available
28
+ * @private
29
+ */
30
+ _checkKeytar() {
31
+ try {
32
+ // Try to import keytar (optional dependency)
33
+ // This will be installed separately by users who want OS keychain integration
34
+ const keytar = require('keytar');
35
+ return !!keytar;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get encryption key from machine-specific data
43
+ * @private
44
+ */
45
+ _getEncryptionKey() {
46
+ // Create a machine-specific key using hostname and homedir
47
+ const { hostname } = require('node:os');
48
+ const machineId = `${hostname()}-${homedir()}`;
49
+
50
+ // Derive key using PBKDF2
51
+ const salt = 'git-super-credential-store-v1';
52
+ return pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
53
+ }
54
+
55
+ /**
56
+ * Encrypt data
57
+ * @private
58
+ */
59
+ _encrypt(data) {
60
+ const key = this._getEncryptionKey();
61
+ const iv = randomBytes(16);
62
+ const cipher = createCipheriv('aes-256-cbc', key, iv);
63
+
64
+ let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
65
+ encrypted += cipher.final('hex');
66
+
67
+ return {
68
+ iv: iv.toString('hex'),
69
+ data: encrypted,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Decrypt data
75
+ * @private
76
+ */
77
+ _decrypt(encrypted) {
78
+ const key = this._getEncryptionKey();
79
+ const iv = Buffer.from(encrypted.iv, 'hex');
80
+ const decipher = createDecipheriv('aes-256-cbc', key, iv);
81
+
82
+ let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
83
+ decrypted += decipher.final('utf8');
84
+
85
+ return JSON.parse(decrypted);
86
+ }
87
+
88
+ /**
89
+ * Get credential from storage
90
+ * @param {string} service - Service name (e.g., 'git-super-github-copilot')
91
+ * @returns {Promise<Object|null>}
92
+ */
93
+ async get(service) {
94
+ // Try keytar first if available
95
+ if (this.keytarAvailable) {
96
+ try {
97
+ const keytar = require('keytar');
98
+ const data = await keytar.getPassword(service, 'default');
99
+ return data ? JSON.parse(data) : null;
100
+ } catch (error) {
101
+ console.warn(`Keytar read failed, falling back to file: ${error.message}`);
102
+ }
103
+ }
104
+
105
+ // Fallback to encrypted file
106
+ return this._getFromFile(service);
107
+ }
108
+
109
+ /**
110
+ * Store credential
111
+ * @param {string} service - Service name
112
+ * @param {Object} data - Credential data
113
+ */
114
+ async set(service, data) {
115
+ // Try keytar first if available
116
+ if (this.keytarAvailable) {
117
+ try {
118
+ const keytar = require('keytar');
119
+ await keytar.setPassword(service, 'default', JSON.stringify(data));
120
+ return;
121
+ } catch (error) {
122
+ console.warn(`Keytar write failed, falling back to file: ${error.message}`);
123
+ }
124
+ }
125
+
126
+ // Fallback to encrypted file
127
+ this._setInFile(service, data);
128
+ }
129
+
130
+ /**
131
+ * Delete credential
132
+ * @param {string} service - Service name
133
+ */
134
+ async delete(service) {
135
+ // Try keytar first if available
136
+ if (this.keytarAvailable) {
137
+ try {
138
+ const keytar = require('keytar');
139
+ await keytar.deletePassword(service, 'default');
140
+ } catch (error) {
141
+ console.warn(`Keytar delete failed: ${error.message}`);
142
+ }
143
+ }
144
+
145
+ // Also delete from file
146
+ this._deleteFromFile(service);
147
+ }
148
+
149
+ /**
150
+ * Get from encrypted file
151
+ * @private
152
+ */
153
+ _getFromFile(service) {
154
+ if (!existsSync(this.storageFile)) {
155
+ return null;
156
+ }
157
+
158
+ try {
159
+ const content = readFileSync(this.storageFile, 'utf8');
160
+ const allData = this._decrypt(JSON.parse(content));
161
+ return allData[service] || null;
162
+ } catch (error) {
163
+ console.error(`Error reading credentials file: ${error.message}`);
164
+ return null;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Set in encrypted file
170
+ * @private
171
+ */
172
+ _setInFile(service, data) {
173
+ let allData = {};
174
+
175
+ // Read existing data
176
+ if (existsSync(this.storageFile)) {
177
+ try {
178
+ const content = readFileSync(this.storageFile, 'utf8');
179
+ allData = this._decrypt(JSON.parse(content));
180
+ } catch (error) {
181
+ console.warn(`Could not read existing credentials: ${error.message}`);
182
+ }
183
+ }
184
+
185
+ // Update data
186
+ allData[service] = data;
187
+
188
+ // Encrypt and write
189
+ const encrypted = this._encrypt(allData);
190
+ writeFileSync(this.storageFile, JSON.stringify(encrypted), { mode: 0o600 });
191
+ }
192
+
193
+ /**
194
+ * Delete from encrypted file
195
+ * @private
196
+ */
197
+ _deleteFromFile(service) {
198
+ if (!existsSync(this.storageFile)) {
199
+ return;
200
+ }
201
+
202
+ try {
203
+ const content = readFileSync(this.storageFile, 'utf8');
204
+ const allData = this._decrypt(JSON.parse(content));
205
+
206
+ delete allData[service];
207
+
208
+ const encrypted = this._encrypt(allData);
209
+ writeFileSync(this.storageFile, JSON.stringify(encrypted), { mode: 0o600 });
210
+ } catch (error) {
211
+ console.error(`Error deleting from credentials file: ${error.message}`);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Get storage method being used
217
+ * @returns {string} 'keytar' or 'file'
218
+ */
219
+ getStorageMethod() {
220
+ return this.keytarAvailable ? 'keytar' : 'file';
221
+ }
222
+ }