bgit-cli 2.0.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/.claude/settings.local.json +19 -0
- package/.env.local +2 -0
- package/CLAUDE_PLAN.md +621 -0
- package/IMPLEMENTATION_REPORT.md +1690 -0
- package/README.md +277 -0
- package/UNIVERSAL_PLAN.md +31 -0
- package/handcash.js +36 -0
- package/index.js +158 -0
- package/index.js.backup +69 -0
- package/lib/auth.js +273 -0
- package/lib/banner.js +17 -0
- package/lib/command-router.js +191 -0
- package/lib/commands.js +157 -0
- package/lib/config.js +438 -0
- package/lib/constants.js +57 -0
- package/lib/crypto.js +164 -0
- package/lib/oauth-server.js +300 -0
- package/lib/payment.js +287 -0
- package/lib/token-manager.js +179 -0
- package/package.json +45 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bgit OAuth Callback Server
|
|
3
|
+
*
|
|
4
|
+
* Temporary local HTTP server for capturing HandCash OAuth callbacks.
|
|
5
|
+
* Runs on localhost:3000-3010 with 5-minute timeout.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Automatic port conflict resolution (tries 3000-3010)
|
|
9
|
+
* - Timeout handling (5 minutes)
|
|
10
|
+
* - Graceful shutdown after token capture
|
|
11
|
+
* - User-friendly success/error pages
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const express = require('express');
|
|
15
|
+
const http = require('http');
|
|
16
|
+
const {
|
|
17
|
+
OAUTH_PORT_START,
|
|
18
|
+
OAUTH_PORT_END,
|
|
19
|
+
OAUTH_TIMEOUT_MS,
|
|
20
|
+
OAUTH_HOST
|
|
21
|
+
} = require('./constants');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Start temporary OAuth callback server
|
|
25
|
+
*
|
|
26
|
+
* @param {number} startPort - First port to try (default: 3000)
|
|
27
|
+
* @param {number} endPort - Last port to try (default: 3010)
|
|
28
|
+
* @returns {Promise<{authToken: string, port: number, server: http.Server}>}
|
|
29
|
+
* @throws {Error} If all ports are in use or timeout occurs
|
|
30
|
+
*/
|
|
31
|
+
async function startOAuthServer(startPort = OAUTH_PORT_START, endPort = OAUTH_PORT_END) {
|
|
32
|
+
const app = express();
|
|
33
|
+
let server;
|
|
34
|
+
let actualPort;
|
|
35
|
+
let resolveToken;
|
|
36
|
+
let rejectToken;
|
|
37
|
+
|
|
38
|
+
// Create promise for token capture
|
|
39
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
40
|
+
resolveToken = resolve;
|
|
41
|
+
rejectToken = reject;
|
|
42
|
+
|
|
43
|
+
// Set timeout (5 minutes)
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
reject(new Error('OAuth timeout: No authorization received within 5 minutes'));
|
|
46
|
+
}, OAUTH_TIMEOUT_MS);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Success callback route
|
|
50
|
+
app.get('/callback', (req, res) => {
|
|
51
|
+
const { authToken } = req.query;
|
|
52
|
+
|
|
53
|
+
if (authToken) {
|
|
54
|
+
res.send(`
|
|
55
|
+
<!DOCTYPE html>
|
|
56
|
+
<html>
|
|
57
|
+
<head>
|
|
58
|
+
<title>bgit Authentication Successful</title>
|
|
59
|
+
<style>
|
|
60
|
+
body {
|
|
61
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
62
|
+
display: flex;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
align-items: center;
|
|
65
|
+
height: 100vh;
|
|
66
|
+
margin: 0;
|
|
67
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
68
|
+
}
|
|
69
|
+
.container {
|
|
70
|
+
background: white;
|
|
71
|
+
padding: 3rem;
|
|
72
|
+
border-radius: 10px;
|
|
73
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
74
|
+
text-align: center;
|
|
75
|
+
max-width: 400px;
|
|
76
|
+
}
|
|
77
|
+
h1 {
|
|
78
|
+
color: #2d3748;
|
|
79
|
+
margin-bottom: 1rem;
|
|
80
|
+
}
|
|
81
|
+
.success-icon {
|
|
82
|
+
font-size: 4rem;
|
|
83
|
+
margin-bottom: 1rem;
|
|
84
|
+
}
|
|
85
|
+
p {
|
|
86
|
+
color: #4a5568;
|
|
87
|
+
line-height: 1.6;
|
|
88
|
+
}
|
|
89
|
+
.close-hint {
|
|
90
|
+
margin-top: 2rem;
|
|
91
|
+
font-size: 0.875rem;
|
|
92
|
+
color: #718096;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
95
|
+
</head>
|
|
96
|
+
<body>
|
|
97
|
+
<div class="container">
|
|
98
|
+
<div class="success-icon">✅</div>
|
|
99
|
+
<h1>Authentication Successful!</h1>
|
|
100
|
+
<p>Your HandCash wallet has been connected to bgit.</p>
|
|
101
|
+
<p>You can now close this window and return to your terminal.</p>
|
|
102
|
+
<div class="close-hint">This window will close automatically in 5 seconds...</div>
|
|
103
|
+
</div>
|
|
104
|
+
<script>
|
|
105
|
+
setTimeout(() => window.close(), 5000);
|
|
106
|
+
</script>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
// Resolve with token
|
|
112
|
+
resolveToken({ authToken, port: actualPort, server });
|
|
113
|
+
} else {
|
|
114
|
+
res.status(400).send(`
|
|
115
|
+
<!DOCTYPE html>
|
|
116
|
+
<html>
|
|
117
|
+
<head>
|
|
118
|
+
<title>bgit Authentication Failed</title>
|
|
119
|
+
<style>
|
|
120
|
+
body {
|
|
121
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
122
|
+
display: flex;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
align-items: center;
|
|
125
|
+
height: 100vh;
|
|
126
|
+
margin: 0;
|
|
127
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
128
|
+
}
|
|
129
|
+
.container {
|
|
130
|
+
background: white;
|
|
131
|
+
padding: 3rem;
|
|
132
|
+
border-radius: 10px;
|
|
133
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
134
|
+
text-align: center;
|
|
135
|
+
max-width: 400px;
|
|
136
|
+
}
|
|
137
|
+
h1 {
|
|
138
|
+
color: #2d3748;
|
|
139
|
+
margin-bottom: 1rem;
|
|
140
|
+
}
|
|
141
|
+
.error-icon {
|
|
142
|
+
font-size: 4rem;
|
|
143
|
+
margin-bottom: 1rem;
|
|
144
|
+
}
|
|
145
|
+
p {
|
|
146
|
+
color: #4a5568;
|
|
147
|
+
line-height: 1.6;
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="container">
|
|
153
|
+
<div class="error-icon">❌</div>
|
|
154
|
+
<h1>Authentication Failed</h1>
|
|
155
|
+
<p>No authorization token received from HandCash.</p>
|
|
156
|
+
<p>Please try again by running: <code>bgit auth login</code></p>
|
|
157
|
+
</div>
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|
|
160
|
+
`);
|
|
161
|
+
|
|
162
|
+
rejectToken(new Error('No auth token received in callback'));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Error callback route
|
|
167
|
+
app.get('/error', (req, res) => {
|
|
168
|
+
const { message } = req.query;
|
|
169
|
+
|
|
170
|
+
res.status(400).send(`
|
|
171
|
+
<!DOCTYPE html>
|
|
172
|
+
<html>
|
|
173
|
+
<head>
|
|
174
|
+
<title>bgit Authentication Error</title>
|
|
175
|
+
<style>
|
|
176
|
+
body {
|
|
177
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
178
|
+
display: flex;
|
|
179
|
+
justify-content: center;
|
|
180
|
+
align-items: center;
|
|
181
|
+
height: 100vh;
|
|
182
|
+
margin: 0;
|
|
183
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
184
|
+
}
|
|
185
|
+
.container {
|
|
186
|
+
background: white;
|
|
187
|
+
padding: 3rem;
|
|
188
|
+
border-radius: 10px;
|
|
189
|
+
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
190
|
+
text-align: center;
|
|
191
|
+
max-width: 400px;
|
|
192
|
+
}
|
|
193
|
+
h1 {
|
|
194
|
+
color: #2d3748;
|
|
195
|
+
margin-bottom: 1rem;
|
|
196
|
+
}
|
|
197
|
+
.error-icon {
|
|
198
|
+
font-size: 4rem;
|
|
199
|
+
margin-bottom: 1rem;
|
|
200
|
+
}
|
|
201
|
+
p {
|
|
202
|
+
color: #4a5568;
|
|
203
|
+
line-height: 1.6;
|
|
204
|
+
}
|
|
205
|
+
.error-message {
|
|
206
|
+
background: #fed7d7;
|
|
207
|
+
padding: 1rem;
|
|
208
|
+
border-radius: 5px;
|
|
209
|
+
margin-top: 1rem;
|
|
210
|
+
color: #c53030;
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
213
|
+
</head>
|
|
214
|
+
<body>
|
|
215
|
+
<div class="container">
|
|
216
|
+
<div class="error-icon">⚠️</div>
|
|
217
|
+
<h1>Authentication Error</h1>
|
|
218
|
+
<p>An error occurred during authentication:</p>
|
|
219
|
+
<div class="error-message">${message || 'Unknown error'}</div>
|
|
220
|
+
<p style="margin-top: 2rem;">Please try again by running: <code>bgit auth login</code></p>
|
|
221
|
+
</div>
|
|
222
|
+
</body>
|
|
223
|
+
</html>
|
|
224
|
+
`);
|
|
225
|
+
|
|
226
|
+
rejectToken(new Error(`OAuth error: ${message || 'Unknown error'}`));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Health check route
|
|
230
|
+
app.get('/health', (req, res) => {
|
|
231
|
+
res.send('OK');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Try to start server on available port
|
|
235
|
+
for (let port = startPort; port <= endPort; port++) {
|
|
236
|
+
try {
|
|
237
|
+
server = await new Promise((resolve, reject) => {
|
|
238
|
+
const srv = http.createServer(app);
|
|
239
|
+
|
|
240
|
+
srv.listen(port, OAUTH_HOST, () => {
|
|
241
|
+
actualPort = port;
|
|
242
|
+
resolve(srv);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
srv.on('error', (err) => {
|
|
246
|
+
if (err.code === 'EADDRINUSE') {
|
|
247
|
+
resolve(null); // Try next port
|
|
248
|
+
} else {
|
|
249
|
+
reject(err);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (server) {
|
|
255
|
+
console.log(`OAuth server started on http://${OAUTH_HOST}:${actualPort}`);
|
|
256
|
+
break; // Successfully started
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw new Error(`Failed to start OAuth server: ${error.message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (!server) {
|
|
264
|
+
throw new Error(`All ports (${startPort}-${endPort}) are in use. Please free up a port and try again.`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Wait for token or timeout
|
|
268
|
+
const result = await tokenPromise;
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Stop OAuth server
|
|
275
|
+
*
|
|
276
|
+
* @param {http.Server} server - Server instance to stop
|
|
277
|
+
* @returns {Promise<void>}
|
|
278
|
+
*/
|
|
279
|
+
function stopOAuthServer(server) {
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
if (!server) {
|
|
282
|
+
resolve();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
server.close((err) => {
|
|
287
|
+
if (err) {
|
|
288
|
+
reject(new Error(`Failed to stop OAuth server: ${err.message}`));
|
|
289
|
+
} else {
|
|
290
|
+
console.log('OAuth server stopped');
|
|
291
|
+
resolve();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
startOAuthServer,
|
|
299
|
+
stopOAuthServer
|
|
300
|
+
};
|
package/lib/payment.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bgit Payment Gateway
|
|
3
|
+
*
|
|
4
|
+
* Handles BitcoinSV payments via HandCash for commit timestamps and pushes.
|
|
5
|
+
* Enhanced with retry logic, error handling, and user-friendly messages.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Automatic retry with exponential backoff
|
|
9
|
+
* - Pre-flight balance check (optional)
|
|
10
|
+
* - Enhanced error messages
|
|
11
|
+
* - Transaction logging
|
|
12
|
+
* - Soft fail for git operations (git succeeds even if payment fails)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { HandCashConnect } = require('@handcash/handcash-connect');
|
|
16
|
+
const chalk = require('chalk');
|
|
17
|
+
const {
|
|
18
|
+
HANDCASH_APP_ID,
|
|
19
|
+
HANDCASH_APP_SECRET,
|
|
20
|
+
TREASURY_HANDLE,
|
|
21
|
+
PAYMENT_RETRY_MAX_ATTEMPTS,
|
|
22
|
+
PAYMENT_RETRY_BASE_DELAY_MS,
|
|
23
|
+
PAYMENT_RETRY_MAX_DELAY_MS
|
|
24
|
+
} = require('./constants');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Sleep for specified milliseconds
|
|
28
|
+
* @param {number} ms - Milliseconds to sleep
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
function sleep(ms) {
|
|
32
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculate exponential backoff delay
|
|
37
|
+
* @param {number} attempt - Current attempt number (0-indexed)
|
|
38
|
+
* @returns {number} Delay in milliseconds
|
|
39
|
+
*/
|
|
40
|
+
function getBackoffDelay(attempt) {
|
|
41
|
+
const delay = PAYMENT_RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
42
|
+
return Math.min(delay, PAYMENT_RETRY_MAX_DELAY_MS);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Execute payment with retry logic
|
|
47
|
+
*
|
|
48
|
+
* @param {number} amount - Amount in BSV
|
|
49
|
+
* @param {string} note - Payment description/note (e.g., "Commit: abc123")
|
|
50
|
+
* @param {string} authToken - HandCash auth token
|
|
51
|
+
* @param {Object} options - Additional options
|
|
52
|
+
* @param {boolean} options.checkBalance - Whether to check balance before payment (default: false)
|
|
53
|
+
* @param {number} options.maxRetries - Max retry attempts (default: 3)
|
|
54
|
+
* @returns {Promise<Object>} Payment result with transactionId
|
|
55
|
+
* @throws {Error} If payment fails after all retries
|
|
56
|
+
*/
|
|
57
|
+
async function executePayment(amount, note, authToken, options = {}) {
|
|
58
|
+
const {
|
|
59
|
+
checkBalance = false,
|
|
60
|
+
maxRetries = PAYMENT_RETRY_MAX_ATTEMPTS
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
if (!authToken) {
|
|
64
|
+
throw new Error('Auth token is required for payment');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!amount || amount <= 0) {
|
|
68
|
+
throw new Error('Payment amount must be greater than 0');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const handCashConnect = new HandCashConnect({
|
|
73
|
+
appId: HANDCASH_APP_ID,
|
|
74
|
+
appSecret: HANDCASH_APP_SECRET
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const account = handCashConnect.getAccountFromAuthToken(authToken);
|
|
78
|
+
|
|
79
|
+
// Optional: Pre-flight balance check
|
|
80
|
+
if (checkBalance) {
|
|
81
|
+
try {
|
|
82
|
+
const balance = await account.wallet.getSpendableBalance();
|
|
83
|
+
|
|
84
|
+
if (balance.bsv < amount) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Insufficient balance. You have ${balance.bsv.toFixed(6)} BSV, but need ${amount} BSV.\n` +
|
|
87
|
+
`Add funds at: ${chalk.cyan('https://handcash.io')}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} catch (balanceError) {
|
|
91
|
+
// Don't fail payment if balance check fails, just warn
|
|
92
|
+
console.warn(chalk.yellow(`Warning: Could not check balance - ${balanceError.message}`));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Payment parameters
|
|
97
|
+
const paymentParameters = {
|
|
98
|
+
description: note || 'bgit payment',
|
|
99
|
+
payments: [
|
|
100
|
+
{
|
|
101
|
+
destination: TREASURY_HANDLE,
|
|
102
|
+
currencyCode: 'BSV',
|
|
103
|
+
sendAmount: amount
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Retry logic with exponential backoff
|
|
109
|
+
let lastError;
|
|
110
|
+
|
|
111
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
112
|
+
try {
|
|
113
|
+
const result = await account.wallet.pay(paymentParameters);
|
|
114
|
+
|
|
115
|
+
// Payment successful
|
|
116
|
+
console.log(chalk.green(`✓ Payment sent! Transaction ID: ${result.transactionId}`));
|
|
117
|
+
|
|
118
|
+
if (note) {
|
|
119
|
+
console.log(chalk.gray(` Note: ${note}`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
lastError = error;
|
|
125
|
+
|
|
126
|
+
// Check if error is retryable
|
|
127
|
+
const isRetryable = isRetryableError(error);
|
|
128
|
+
|
|
129
|
+
if (!isRetryable || attempt === maxRetries - 1) {
|
|
130
|
+
// Don't retry, throw error
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Retry with backoff
|
|
135
|
+
const delay = getBackoffDelay(attempt);
|
|
136
|
+
console.log(chalk.yellow(`⚠️ Payment attempt ${attempt + 1} failed, retrying in ${delay / 1000}s... (${attempt + 1}/${maxRetries})`));
|
|
137
|
+
await sleep(delay);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// All retries failed
|
|
142
|
+
throw enhancePaymentError(lastError);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if error is retryable
|
|
150
|
+
* Network errors and timeouts are retryable
|
|
151
|
+
* Insufficient balance and invalid tokens are not
|
|
152
|
+
*
|
|
153
|
+
* @param {Error} error - Error to check
|
|
154
|
+
* @returns {boolean} true if error is retryable
|
|
155
|
+
*/
|
|
156
|
+
function isRetryableError(error) {
|
|
157
|
+
const message = error.message.toLowerCase();
|
|
158
|
+
|
|
159
|
+
// Retryable errors
|
|
160
|
+
const retryableKeywords = [
|
|
161
|
+
'network',
|
|
162
|
+
'timeout',
|
|
163
|
+
'econnrefused',
|
|
164
|
+
'enotfound',
|
|
165
|
+
'etimedout',
|
|
166
|
+
'socket hang up'
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
if (retryableKeywords.some(keyword => message.includes(keyword))) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Non-retryable errors
|
|
174
|
+
const nonRetryableKeywords = [
|
|
175
|
+
'insufficient',
|
|
176
|
+
'balance',
|
|
177
|
+
'invalid token',
|
|
178
|
+
'unauthorized',
|
|
179
|
+
'forbidden'
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
if (nonRetryableKeywords.some(keyword => message.includes(keyword))) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Default: retry unknown errors
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Enhance payment error with helpful user message
|
|
192
|
+
*
|
|
193
|
+
* @param {Error} error - Original error
|
|
194
|
+
* @returns {Error} Enhanced error with user-friendly message
|
|
195
|
+
*/
|
|
196
|
+
function enhancePaymentError(error) {
|
|
197
|
+
const message = error.message.toLowerCase();
|
|
198
|
+
|
|
199
|
+
// Insufficient balance
|
|
200
|
+
if (message.includes('insufficient') || message.includes('balance')) {
|
|
201
|
+
return new Error(
|
|
202
|
+
`Insufficient balance in your HandCash wallet.\n` +
|
|
203
|
+
`Add funds at: ${chalk.cyan('https://handcash.io')}\n` +
|
|
204
|
+
`Original error: ${error.message}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Invalid auth
|
|
209
|
+
if (message.includes('unauthorized') || message.includes('invalid token')) {
|
|
210
|
+
return new Error(
|
|
211
|
+
`Your authentication has expired.\n` +
|
|
212
|
+
`Please run: ${chalk.cyan('bgit auth login')}\n` +
|
|
213
|
+
`Original error: ${error.message}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Network errors
|
|
218
|
+
if (message.includes('network') || message.includes('timeout')) {
|
|
219
|
+
return new Error(
|
|
220
|
+
`Network error - please check your internet connection and try again.\n` +
|
|
221
|
+
`Original error: ${error.message}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// API errors
|
|
226
|
+
if (message.includes('api') || message.includes('handcash')) {
|
|
227
|
+
return new Error(
|
|
228
|
+
`HandCash API error - the service may be temporarily unavailable.\n` +
|
|
229
|
+
`Please try again later.\n` +
|
|
230
|
+
`Original error: ${error.message}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Unknown error
|
|
235
|
+
return new Error(`Payment failed: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Format payment note for commit timestamp
|
|
240
|
+
*
|
|
241
|
+
* @param {string} commitHash - Git commit hash
|
|
242
|
+
* @returns {string} Formatted note
|
|
243
|
+
*/
|
|
244
|
+
function formatCommitNote(commitHash) {
|
|
245
|
+
return `bgit commit: ${commitHash}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Format payment note for push
|
|
250
|
+
*
|
|
251
|
+
* @param {string} remote - Remote name (e.g., "origin")
|
|
252
|
+
* @param {string} branch - Branch name (e.g., "main")
|
|
253
|
+
* @returns {string} Formatted note
|
|
254
|
+
*/
|
|
255
|
+
function formatPushNote(remote = 'origin', branch = 'main') {
|
|
256
|
+
return `bgit push: ${remote}/${branch}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Soft fail wrapper for payments
|
|
261
|
+
* If payment fails, warn user but don't fail the git operation
|
|
262
|
+
*
|
|
263
|
+
* @param {Function} paymentFn - Payment function to execute
|
|
264
|
+
* @param {string} operation - Operation name (e.g., "commit", "push")
|
|
265
|
+
* @returns {Promise<boolean>} true if payment succeeded, false otherwise
|
|
266
|
+
*/
|
|
267
|
+
async function softFailPayment(paymentFn, operation) {
|
|
268
|
+
try {
|
|
269
|
+
await paymentFn();
|
|
270
|
+
return true;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.warn(chalk.yellow(`\n⚠️ ${operation} succeeded, but on-chain timestamp failed:`));
|
|
273
|
+
console.warn(chalk.yellow(error.message));
|
|
274
|
+
console.log(chalk.gray(`Your ${operation} was saved locally.`));
|
|
275
|
+
console.log();
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = {
|
|
281
|
+
executePayment,
|
|
282
|
+
formatCommitNote,
|
|
283
|
+
formatPushNote,
|
|
284
|
+
softFailPayment,
|
|
285
|
+
isRetryableError,
|
|
286
|
+
enhancePaymentError
|
|
287
|
+
};
|