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/lib/auth.js ADDED
@@ -0,0 +1,273 @@
1
+ /**
2
+ * bgit Authentication Manager
3
+ *
4
+ * Orchestrates HandCash OAuth authentication flow.
5
+ * Manages token lifecycle and user authentication state.
6
+ *
7
+ * Flow:
8
+ * 1. Check for existing token
9
+ * 2. If no token or invalid → trigger OAuth
10
+ * 3. Start local OAuth server
11
+ * 4. Open browser to HandCash
12
+ * 5. Capture callback with token
13
+ * 6. Encrypt and save token
14
+ * 7. Validate token
15
+ */
16
+
17
+ const { HandCashConnect } = require('@handcash/handcash-connect');
18
+ const open = require('open');
19
+ const chalk = require('chalk');
20
+ const { loadToken, saveToken, deleteToken, hasToken } = require('./config');
21
+ const { isTokenValid, getProfile, getBalance, clearValidationCache } = require('./token-manager');
22
+ const { startOAuthServer, stopOAuthServer } = require('./oauth-server');
23
+ const { HANDCASH_APP_ID, HANDCASH_APP_SECRET } = require('./constants');
24
+
25
+ /**
26
+ * Ensure user is authenticated
27
+ * Checks for existing valid token or triggers OAuth flow
28
+ *
29
+ * @param {boolean} silent - If true, don't show welcome messages
30
+ * @returns {Promise<string>} Valid auth token
31
+ * @throws {Error} If authentication fails
32
+ */
33
+ async function ensureAuthenticated(silent = false) {
34
+ try {
35
+ // Check if token exists
36
+ if (hasToken()) {
37
+ const authToken = loadToken();
38
+
39
+ if (authToken) {
40
+ // Validate token
41
+ const valid = await isTokenValid(authToken);
42
+
43
+ if (valid) {
44
+ return authToken; // Token is valid, proceed
45
+ } else {
46
+ console.log(chalk.yellow('⚠️ Your authentication has expired.'));
47
+ console.log(chalk.yellow('Re-authenticating...\n'));
48
+ }
49
+ }
50
+ }
51
+
52
+ // No token or invalid → trigger OAuth
53
+ if (!silent) {
54
+ console.log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
55
+ console.log(chalk.blue.bold('🚀 Welcome to bgit!'));
56
+ console.log();
57
+ console.log('bgit timestamps your commits on BitcoinSV using HandCash.');
58
+ console.log('Let\'s set up your authentication...');
59
+ console.log(chalk.blue('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
60
+ }
61
+
62
+ const authToken = await initiateOAuthFlow();
63
+ return authToken;
64
+ } catch (error) {
65
+ throw new Error(`Authentication failed: ${error.message}`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Initiate full OAuth authentication flow
71
+ *
72
+ * @returns {Promise<string>} Valid auth token
73
+ * @throws {Error} If OAuth flow fails
74
+ */
75
+ async function initiateOAuthFlow() {
76
+ let server = null;
77
+
78
+ try {
79
+ console.log(chalk.cyan('Opening browser for HandCash authorization...'));
80
+
81
+ // 1. Start OAuth callback server
82
+ const serverPromise = startOAuthServer();
83
+
84
+ // 2. Get HandCash redirect URL
85
+ const handCashConnect = new HandCashConnect({
86
+ appId: HANDCASH_APP_ID,
87
+ appSecret: HANDCASH_APP_SECRET
88
+ });
89
+
90
+ const redirectUrl = handCashConnect.getRedirectionUrl();
91
+
92
+ // 3. Open browser
93
+ try {
94
+ await open(redirectUrl);
95
+ console.log(chalk.green('✓ Browser opened'));
96
+ } catch (error) {
97
+ console.log(chalk.yellow('\n⚠️ Unable to open browser automatically'));
98
+ console.log(chalk.yellow('Please visit this URL to authorize:\n'));
99
+ console.log(chalk.cyan(redirectUrl));
100
+ console.log();
101
+ }
102
+
103
+ console.log(chalk.cyan('Waiting for authorization... ⏳\n'));
104
+
105
+ // 4. Wait for callback
106
+ const result = await serverPromise;
107
+ const { authToken, server: srv } = result;
108
+ server = srv;
109
+
110
+ // 5. Validate token
111
+ console.log(chalk.cyan('Validating token...'));
112
+ const valid = await isTokenValid(authToken);
113
+
114
+ if (!valid) {
115
+ throw new Error('Received invalid token from HandCash');
116
+ }
117
+
118
+ // 6. Save encrypted token
119
+ saveToken(authToken);
120
+ clearValidationCache(); // Clear cache to force fresh validation next time
121
+
122
+ // 7. Stop server
123
+ await stopOAuthServer(server);
124
+
125
+ console.log(chalk.green('✓ Authentication successful!'));
126
+ console.log(chalk.green('✓ Credentials saved securely\n'));
127
+
128
+ return authToken;
129
+ } catch (error) {
130
+ // Clean up server on error
131
+ if (server) {
132
+ try {
133
+ await stopOAuthServer(server);
134
+ } catch (stopError) {
135
+ // Ignore cleanup errors
136
+ }
137
+ }
138
+
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Handle `bgit auth login` command
145
+ * Force re-authentication even if token exists
146
+ *
147
+ * @returns {Promise<void>}
148
+ */
149
+ async function loginCommand() {
150
+ try {
151
+ console.log(chalk.blue.bold('\n🔐 bgit Authentication\n'));
152
+
153
+ // Delete existing token
154
+ if (hasToken()) {
155
+ deleteToken();
156
+ clearValidationCache();
157
+ console.log(chalk.yellow('Cleared existing credentials'));
158
+ }
159
+
160
+ // Trigger OAuth
161
+ await initiateOAuthFlow();
162
+
163
+ console.log(chalk.green.bold('✓ Login successful!\n'));
164
+ } catch (error) {
165
+ console.error(chalk.red(`\n❌ Login failed: ${error.message}\n`));
166
+ process.exit(1);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Handle `bgit auth logout` command
172
+ * Delete saved token
173
+ *
174
+ * @returns {Promise<void>}
175
+ */
176
+ async function logoutCommand() {
177
+ try {
178
+ console.log(chalk.blue.bold('\n🔓 bgit Logout\n'));
179
+
180
+ if (!hasToken()) {
181
+ console.log(chalk.yellow('You are not currently logged in'));
182
+ console.log(chalk.gray('Run: bgit auth login\n'));
183
+ return;
184
+ }
185
+
186
+ deleteToken();
187
+ clearValidationCache();
188
+
189
+ console.log(chalk.green('✓ Logged out successfully'));
190
+ console.log(chalk.gray('Your commits are safe, but timestamping is now disabled'));
191
+ console.log(chalk.gray('Run: bgit auth login\n'));
192
+ } catch (error) {
193
+ console.error(chalk.red(`\n❌ Logout failed: ${error.message}\n`));
194
+ process.exit(1);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Handle `bgit auth status` command
200
+ * Show authentication status and account info
201
+ *
202
+ * @returns {Promise<void>}
203
+ */
204
+ async function statusCommand() {
205
+ try {
206
+ console.log(chalk.blue.bold('\n🔍 bgit Authentication Status\n'));
207
+
208
+ if (!hasToken()) {
209
+ console.log(chalk.yellow('Status: Not authenticated'));
210
+ console.log(chalk.gray('Run: bgit auth login\n'));
211
+ return;
212
+ }
213
+
214
+ const authToken = loadToken();
215
+ console.log(chalk.cyan('Validating token...'));
216
+
217
+ const valid = await isTokenValid(authToken);
218
+
219
+ if (!valid) {
220
+ console.log(chalk.red('Status: Invalid/expired token'));
221
+ console.log(chalk.gray('Run: bgit auth login\n'));
222
+ return;
223
+ }
224
+
225
+ console.log(chalk.green('✓ Status: Authenticated\n'));
226
+
227
+ // Get profile info
228
+ const profile = await getProfile(authToken);
229
+
230
+ if (profile) {
231
+ console.log(chalk.bold('Account Information:'));
232
+ console.log(` Handle: ${chalk.cyan('@' + profile.handle)}`);
233
+ console.log(` Name: ${profile.publicProfile?.displayName || 'N/A'}`);
234
+
235
+ if (profile.publicProfile?.avatarUrl) {
236
+ console.log(` Avatar: ${profile.publicProfile.avatarUrl}`);
237
+ }
238
+
239
+ console.log();
240
+ }
241
+
242
+ // Get balance
243
+ const balance = await getBalance(authToken);
244
+
245
+ if (balance) {
246
+ console.log(chalk.bold('Wallet Balance:'));
247
+
248
+ // HandCash returns balance per currency
249
+ if (balance.bsv) {
250
+ console.log(` BSV: ${chalk.green(balance.bsv.toFixed(6))} BSV`);
251
+ }
252
+
253
+ if (balance.usd) {
254
+ console.log(` USD: ${chalk.green('$' + balance.usd.toFixed(2))}`);
255
+ }
256
+
257
+ console.log();
258
+ }
259
+
260
+ console.log(chalk.gray('Token is valid and ready for use\n'));
261
+ } catch (error) {
262
+ console.error(chalk.red(`\n❌ Status check failed: ${error.message}\n`));
263
+ process.exit(1);
264
+ }
265
+ }
266
+
267
+ module.exports = {
268
+ ensureAuthenticated,
269
+ initiateOAuthFlow,
270
+ loginCommand,
271
+ logoutCommand,
272
+ statusCommand
273
+ };
package/lib/banner.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * bgit Banner
3
+ * Displays the CLI ASCII art
4
+ */
5
+ module.exports = function showBanner() {
6
+ console.log(`
7
+ \x1b[36m██████╗\x1b[0m \x1b[33m██████╗\x1b[0m \x1b[36m██╗████████╗\x1b[0m
8
+ \x1b[36m██╔══██╗\x1b[0m \x1b[33m██╔════╝\x1b[0m \x1b[36m██║╚══██╔══╝\x1b[0m
9
+ \x1b[36m██████╔╝\x1b[0m \x1b[33m██║ ███╗\x1b[0m \x1b[36m██║ ██║\x1b[0m
10
+ \x1b[36m██╔══██╗\x1b[0m \x1b[33m██║ ██║\x1b[0m \x1b[36m██║ ██║\x1b[0m
11
+ \x1b[36m██████╔╝\x1b[0m \x1b[35m██╗\x1b[0m \x1b[33m╚██████╔╝\x1b[0m \x1b[36m██║ ██║\x1b[0m
12
+ \x1b[36m╚═════╝\x1b[0m \x1b[35m╚═╝\x1b[0m \x1b[33m╚═════╝\x1b[0m \x1b[36m╚═╝ ╚═╝\x1b[0m
13
+
14
+ \x1b[90m> Bitcoin-Native Git Wrapper\x1b[0m
15
+ \x1b[90m> Pay-to-Operate • Universal History\x1b[0m
16
+ `);
17
+ };
@@ -0,0 +1,191 @@
1
+ /**
2
+ * bgit Command Router
3
+ *
4
+ * Routes git commands to appropriate execution handlers:
5
+ * - Payment-gated commands: commit, push
6
+ * - Pass-through commands: all others (status, log, diff, etc.)
7
+ *
8
+ * Payment Logic:
9
+ * - commit: Git commit FIRST, then timestamp hash on-chain
10
+ * - push: Payment FIRST (gatekeeper), then git push
11
+ */
12
+
13
+ const { spawn, execSync } = require('child_process');
14
+ const chalk = require('chalk');
15
+ const { executePayment, formatCommitNote, formatPushNote, softFailPayment } = require('./payment');
16
+ const { PAYMENT_AMOUNTS } = require('./constants');
17
+ const { getPaymentGatedCommands } = require('./config');
18
+
19
+ /**
20
+ * Check if command requires payment
21
+ * Uses configurable payment mode from config
22
+ *
23
+ * @param {string} command - Git command (e.g., "commit", "push", "status")
24
+ * @returns {boolean} true if command requires payment
25
+ */
26
+ function isPaymentGated(command) {
27
+ const gatedCommands = getPaymentGatedCommands();
28
+ return gatedCommands.includes(command);
29
+ }
30
+
31
+ /**
32
+ * Execute git command directly (pass-through)
33
+ *
34
+ * @param {string[]} args - Git command arguments (excluding 'git')
35
+ * @returns {Promise<number>} Exit code
36
+ */
37
+ function executeGitCommand(args) {
38
+ return new Promise((resolve) => {
39
+ const gitProcess = spawn('git', args, { stdio: 'inherit' });
40
+
41
+ gitProcess.on('close', (code) => {
42
+ resolve(code);
43
+ });
44
+
45
+ gitProcess.on('error', (error) => {
46
+ console.error(chalk.red(`Failed to execute git: ${error.message}`));
47
+ resolve(1);
48
+ });
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Execute git commit with on-chain timestamp
54
+ * Flow: git commit → get hash → pay for timestamp
55
+ *
56
+ * @param {string[]} args - Git command arguments
57
+ * @param {string} authToken - HandCash auth token
58
+ * @returns {Promise<number>} Exit code
59
+ */
60
+ async function executeCommit(args, authToken) {
61
+ // 1. Execute git commit first
62
+ const exitCode = await executeGitCommand(args);
63
+
64
+ if (exitCode !== 0) {
65
+ // Commit failed, don't try to pay
66
+ return exitCode;
67
+ }
68
+
69
+ // 2. Get the new commit hash
70
+ let commitHash;
71
+ try {
72
+ commitHash = execSync('git rev-parse HEAD').toString().trim();
73
+ } catch (error) {
74
+ console.error(chalk.red(`Failed to get commit hash: ${error.message}`));
75
+ return 0; // Commit succeeded, hash retrieval failed
76
+ }
77
+
78
+ console.log(chalk.blue(`\n🔗 Capturing commit hash: ${chalk.bold(commitHash)}`));
79
+ console.log(chalk.blue('💰 Timestamping this commit on-chain...\n'));
80
+
81
+ // 3. Pay for timestamp (soft fail - don't block if payment fails)
82
+ await softFailPayment(
83
+ async () => {
84
+ await executePayment(
85
+ PAYMENT_AMOUNTS.commit,
86
+ formatCommitNote(commitHash),
87
+ authToken
88
+ );
89
+ console.log(chalk.green.bold('✓ Commit timestamped on BitcoinSV!'));
90
+ },
91
+ 'Commit'
92
+ );
93
+
94
+ return 0; // Commit succeeded
95
+ }
96
+
97
+ /**
98
+ * Execute git push with payment gatekeeper
99
+ * Flow: pay first → then git push
100
+ *
101
+ * @param {string[]} args - Git command arguments
102
+ * @param {string} authToken - HandCash auth token
103
+ * @returns {Promise<number>} Exit code
104
+ */
105
+ async function executePush(args, authToken) {
106
+ // Extract remote and branch from args if available
107
+ let remote = 'origin';
108
+ let branch = 'main';
109
+
110
+ // args might be ['push', 'origin', 'main'] or just ['push']
111
+ if (args.length > 1) {
112
+ remote = args[1];
113
+ }
114
+ if (args.length > 2) {
115
+ branch = args[2];
116
+ }
117
+
118
+ console.log(chalk.blue(`\n💰 Payment required to push to ${remote}/${branch}...`));
119
+
120
+ // 1. Pay first (gatekeeper)
121
+ try {
122
+ await executePayment(
123
+ PAYMENT_AMOUNTS.push,
124
+ formatPushNote(remote, branch),
125
+ authToken
126
+ );
127
+ console.log(chalk.green('✓ Payment successful! Executing push...\n'));
128
+ } catch (error) {
129
+ // Payment failed - block the push
130
+ console.error(chalk.red(`\n❌ Payment failed: ${error.message}`));
131
+ console.error(chalk.red('Push cancelled. Please resolve the issue and try again.\n'));
132
+ return 1;
133
+ }
134
+
135
+ // 2. Execute git push
136
+ const exitCode = await executeGitCommand(args);
137
+
138
+ if (exitCode === 0) {
139
+ console.log(chalk.green.bold('\n✓ Push completed and payment recorded on BitcoinSV!'));
140
+ } else {
141
+ console.warn(chalk.yellow('\n⚠️ Push failed, but payment was already processed.'));
142
+ }
143
+
144
+ return exitCode;
145
+ }
146
+
147
+ /**
148
+ * Route command to appropriate handler
149
+ *
150
+ * @param {string[]} args - Git command arguments (e.g., ['commit', '-m', 'message'])
151
+ * @param {string} authToken - HandCash auth token (required for payment-gated commands)
152
+ * @returns {Promise<number>} Exit code
153
+ */
154
+ async function routeCommand(args, authToken) {
155
+ if (!args || args.length === 0) {
156
+ console.error(chalk.red('No git command specified'));
157
+ return 1;
158
+ }
159
+
160
+ const command = args[0];
161
+
162
+ // Route to appropriate handler
163
+ if (command === 'commit') {
164
+ return await executeCommit(args, authToken);
165
+ } else if (command === 'push') {
166
+ return await executePush(args, authToken);
167
+ } else {
168
+ // Pass through to git
169
+ return await executeGitCommand(args);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Check if command needs authentication
175
+ * Only payment-gated commands need auth
176
+ *
177
+ * @param {string} command - Git command
178
+ * @returns {boolean} true if auth is required
179
+ */
180
+ function needsAuthentication(command) {
181
+ return isPaymentGated(command);
182
+ }
183
+
184
+ module.exports = {
185
+ isPaymentGated,
186
+ needsAuthentication,
187
+ routeCommand,
188
+ executeGitCommand,
189
+ executeCommit,
190
+ executePush
191
+ };
@@ -0,0 +1,157 @@
1
+ module.exports = [
2
+ "add",
3
+ "am",
4
+ "annotate",
5
+ "apply",
6
+ "archimport",
7
+ "archive",
8
+ "backfill",
9
+ "bisect",
10
+ "blame",
11
+ "branch",
12
+ "bugreport",
13
+ "bundle",
14
+ "cat-file",
15
+ "check-attr",
16
+ "check-ignore",
17
+ "check-mailmap",
18
+ "check-ref-format",
19
+ "checkout",
20
+ "checkout-index",
21
+ "cherry",
22
+ "cherry-pick",
23
+ "citool",
24
+ "clean",
25
+ "clone",
26
+ "column",
27
+ "commit",
28
+ "commit-graph",
29
+ "commit-tree",
30
+ "config",
31
+ "count-objects",
32
+ "credential",
33
+ "credential-cache",
34
+ "credential-store",
35
+ "cvsexportcommit",
36
+ "cvsimport",
37
+ "cvsserver",
38
+ "daemon",
39
+ "describe",
40
+ "diagnose",
41
+ "diff",
42
+ "diff-files",
43
+ "diff-index",
44
+ "diff-pairs",
45
+ "diff-tree",
46
+ "difftool",
47
+ "fast-export",
48
+ "fast-import",
49
+ "fetch",
50
+ "fetch-pack",
51
+ "filter-branch",
52
+ "fmt-merge-msg",
53
+ "for-each-ref",
54
+ "for-each-repo",
55
+ "format-patch",
56
+ "fsck",
57
+ "gc",
58
+ "get-tar-commit-id",
59
+ "grep",
60
+ "gui",
61
+ "hash-object",
62
+ "help",
63
+ "hook",
64
+ "http-backend",
65
+ "http-fetch",
66
+ "http-push",
67
+ "imap-send",
68
+ "index-pack",
69
+ "init",
70
+ "instaweb",
71
+ "interpret-trailers",
72
+ "last-modified",
73
+ "log",
74
+ "ls-files",
75
+ "ls-remote",
76
+ "ls-tree",
77
+ "mailinfo",
78
+ "mailsplit",
79
+ "maintenance",
80
+ "merge",
81
+ "merge-base",
82
+ "merge-file",
83
+ "merge-index",
84
+ "merge-one-file",
85
+ "merge-tree",
86
+ "mergetool",
87
+ "mktag",
88
+ "mktree",
89
+ "multi-pack-index",
90
+ "mv",
91
+ "name-rev",
92
+ "notes",
93
+ "p4",
94
+ "pack-objects",
95
+ "pack-redundant",
96
+ "pack-refs",
97
+ "patch-id",
98
+ "prune",
99
+ "prune-packed",
100
+ "pull",
101
+ "push",
102
+ "quiltimport",
103
+ "range-diff",
104
+ "read-tree",
105
+ "rebase",
106
+ "receive-pack",
107
+ "reflog",
108
+ "refs",
109
+ "remote",
110
+ "repack",
111
+ "replace",
112
+ "replay",
113
+ "repo",
114
+ "request-pull",
115
+ "rerere",
116
+ "reset",
117
+ "restore",
118
+ "rev-list",
119
+ "rev-parse",
120
+ "revert",
121
+ "rm",
122
+ "send-email",
123
+ "send-pack",
124
+ "sh-i18n",
125
+ "sh-setup",
126
+ "shell",
127
+ "shortlog",
128
+ "show",
129
+ "show-branch",
130
+ "show-index",
131
+ "show-ref",
132
+ "sparse-checkout",
133
+ "stage",
134
+ "stash",
135
+ "status",
136
+ "stripspace",
137
+ "submodule",
138
+ "svn",
139
+ "switch",
140
+ "symbolic-ref",
141
+ "tag",
142
+ "unpack-file",
143
+ "unpack-objects",
144
+ "update-index",
145
+ "update-ref",
146
+ "update-server-info",
147
+ "upload-archive",
148
+ "upload-pack",
149
+ "var",
150
+ "verify-commit",
151
+ "verify-pack",
152
+ "verify-tag",
153
+ "version",
154
+ "whatchanged",
155
+ "worktree",
156
+ "write-tree"
157
+ ];