@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,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
|
+
}
|