git-slot-machine 2.3.1 → 2.4.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.
Files changed (68) hide show
  1. package/dist/animation/slotMachine.js +47 -44
  2. package/dist/index.js +4 -1
  3. package/dist/templates/post-commit.d.ts +1 -1
  4. package/dist/templates/post-commit.js +3 -11
  5. package/package.json +6 -1
  6. package/.claude/release.md +0 -74
  7. package/CHANGELOG.md +0 -241
  8. package/dist/animation/slotMachine.d.ts.map +0 -1
  9. package/dist/animation/slotMachine.js.map +0 -1
  10. package/dist/api.d.ts.map +0 -1
  11. package/dist/api.js.map +0 -1
  12. package/dist/balance.d.ts.map +0 -1
  13. package/dist/balance.js.map +0 -1
  14. package/dist/commands/auth.d.ts.map +0 -1
  15. package/dist/commands/auth.js.map +0 -1
  16. package/dist/commands/balance.d.ts.map +0 -1
  17. package/dist/commands/balance.js.map +0 -1
  18. package/dist/commands/config.d.ts.map +0 -1
  19. package/dist/commands/config.js.map +0 -1
  20. package/dist/commands/init.d.ts.map +0 -1
  21. package/dist/commands/init.js.map +0 -1
  22. package/dist/commands/play.d.ts.map +0 -1
  23. package/dist/commands/play.js.map +0 -1
  24. package/dist/commands/spin.d.ts.map +0 -1
  25. package/dist/commands/spin.js.map +0 -1
  26. package/dist/commands/sync.d.ts.map +0 -1
  27. package/dist/commands/sync.js.map +0 -1
  28. package/dist/commands/test.d.ts.map +0 -1
  29. package/dist/commands/test.js.map +0 -1
  30. package/dist/config.d.ts.map +0 -1
  31. package/dist/config.js.map +0 -1
  32. package/dist/index.d.ts.map +0 -1
  33. package/dist/index.js.map +0 -1
  34. package/dist/patterns.d.ts.map +0 -1
  35. package/dist/patterns.js.map +0 -1
  36. package/dist/secrets.d.ts.map +0 -1
  37. package/dist/secrets.js.map +0 -1
  38. package/dist/templates/post-commit.d.ts.map +0 -1
  39. package/dist/templates/post-commit.js.map +0 -1
  40. package/dist/utils/amendDetector.d.ts.map +0 -1
  41. package/dist/utils/amendDetector.js.map +0 -1
  42. package/dist/utils/fetch-polyfill.d.ts.map +0 -1
  43. package/dist/utils/fetch-polyfill.js.map +0 -1
  44. package/dist/utils/git.d.ts.map +0 -1
  45. package/dist/utils/git.js.map +0 -1
  46. package/jest.config.js +0 -12
  47. package/src/animation/slotMachine.ts +0 -159
  48. package/src/api.ts +0 -207
  49. package/src/balance.ts +0 -118
  50. package/src/commands/auth.ts +0 -92
  51. package/src/commands/balance.ts +0 -28
  52. package/src/commands/config.ts +0 -59
  53. package/src/commands/init.ts +0 -259
  54. package/src/commands/play.ts +0 -196
  55. package/src/commands/spin.ts +0 -17
  56. package/src/commands/sync.ts +0 -49
  57. package/src/commands/test.ts +0 -19
  58. package/src/config.ts +0 -189
  59. package/src/index.ts +0 -132
  60. package/src/patterns.test.ts +0 -44
  61. package/src/patterns.ts +0 -313
  62. package/src/secrets.ts +0 -44
  63. package/src/templates/post-commit.ts +0 -23
  64. package/src/utils/amendDetector.ts +0 -74
  65. package/src/utils/fetch-polyfill.ts +0 -13
  66. package/src/utils/git.ts +0 -88
  67. package/test.txt +0 -2
  68. package/tsconfig.json +0 -21
@@ -1,92 +0,0 @@
1
- import chalk from 'chalk';
2
- import { createToken, logout as apiLogout, verifyToken } from '../api.js';
3
- import { setApiToken, clearApiToken, getApiToken, getApiUrl, setGitHubUsername } from '../config.js';
4
-
5
- export async function authLoginCommand(githubUsername: string): Promise<void> {
6
- try {
7
- console.log(chalk.dim(`Generating token for ${githubUsername}...`));
8
-
9
- // Generate token from GitHub username
10
- const token = await createToken(githubUsername);
11
-
12
- if (!token) {
13
- console.error(chalk.red('Failed to generate token. Please check your GitHub username.'));
14
- process.exit(1);
15
- }
16
-
17
- // Save token and username
18
- setApiToken(token);
19
- setGitHubUsername(githubUsername);
20
-
21
- console.log(chalk.green('Successfully authenticated!'));
22
- console.log(chalk.dim(`Token saved. API URL: ${getApiUrl()}`));
23
- console.log(chalk.dim(`GitHub Username: ${githubUsername}`));
24
- console.log();
25
- console.log(chalk.yellow('Data sent to server on each commit:'));
26
- console.log(chalk.dim(' • Commit hash (7 and 40 character versions)'));
27
- console.log(chalk.dim(' • Repository URL, owner, and name'));
28
- console.log(chalk.dim(' • GitHub username'));
29
- console.log(chalk.dim(' • Pattern type, payout, and balance'));
30
- console.log();
31
- console.log(chalk.dim('To disable sync: git-slot-machine config set sync-enabled false'));
32
- } catch (error) {
33
- console.error(chalk.red(`Error: ${(error as Error).message}`));
34
- process.exit(1);
35
- }
36
- }
37
-
38
- export async function authLogoutCommand(): Promise<void> {
39
- try {
40
- const token = getApiToken();
41
-
42
- if (!token) {
43
- console.log(chalk.yellow('Not currently authenticated.'));
44
- return;
45
- }
46
-
47
- // Try to revoke token on server
48
- await apiLogout();
49
-
50
- // Clear local token
51
- clearApiToken();
52
-
53
- console.log(chalk.green('Successfully logged out.'));
54
- } catch (error) {
55
- console.error(chalk.red(`Error: ${(error as Error).message}`));
56
- process.exit(1);
57
- }
58
- }
59
-
60
- export async function authStatusCommand(): Promise<void> {
61
- try {
62
- const token = getApiToken();
63
- const apiUrl = getApiUrl();
64
-
65
- if (!token) {
66
- console.log(chalk.yellow('Not authenticated.'));
67
- console.log(chalk.dim(`API URL: ${apiUrl}`));
68
- console.log();
69
- console.log('To authenticate, run:');
70
- console.log(chalk.cyan(' git-slot-machine auth login <your-github-username>'));
71
- return;
72
- }
73
-
74
- // Verify token is still valid
75
- const isValid = await verifyToken(token);
76
-
77
- if (isValid) {
78
- console.log(chalk.green('Authenticated'));
79
- console.log(chalk.dim(`API URL: ${apiUrl}`));
80
- console.log(chalk.dim(`Token: ${token.substring(0, 10)}...`));
81
- } else {
82
- console.log(chalk.red('Authentication expired or invalid.'));
83
- console.log(chalk.dim(`API URL: ${apiUrl}`));
84
- console.log();
85
- console.log('Please login again:');
86
- console.log(chalk.cyan(' git-slot-machine auth login <your-github-username>'));
87
- }
88
- } catch (error) {
89
- console.error(chalk.red(`Error: ${(error as Error).message}`));
90
- process.exit(1);
91
- }
92
- }
@@ -1,28 +0,0 @@
1
- import chalk from 'chalk';
2
- import { getRepoStats } from '../balance.js';
3
-
4
- export function balanceCommand(): void {
5
- try {
6
- const stats = getRepoStats();
7
-
8
- if (!stats) {
9
- console.log(chalk.yellow('No balance data for this repository'));
10
- console.log(chalk.dim('Run a commit or use "git-slot-machine spin" to start playing'));
11
- return;
12
- }
13
-
14
- console.log();
15
- console.log(chalk.cyan.bold('Repository Stats'));
16
- console.log(chalk.dim('━'.repeat(40)));
17
- console.log(chalk.white(`Balance: ${stats.balance >= 0 ? chalk.green(stats.balance) : chalk.red(stats.balance)} points`));
18
- console.log(chalk.white(`Total Commits: ${stats.totalCommits}`));
19
- console.log(chalk.white(`Total Winnings: ${chalk.yellow(stats.totalWinnings)} points`));
20
- console.log(chalk.white(`Biggest Win: ${chalk.yellow(stats.biggestWin)} points`));
21
- console.log(chalk.white(`Last Commit: ${chalk.dim(stats.lastCommit)}`));
22
- console.log();
23
-
24
- } catch (error) {
25
- console.error(chalk.red(`Error: ${(error as Error).message}`));
26
- process.exit(1);
27
- }
28
- }
@@ -1,59 +0,0 @@
1
- import chalk from 'chalk';
2
- import {
3
- getApiUrl,
4
- setApiUrl,
5
- isSyncEnabled,
6
- setSyncEnabled,
7
- getConfig
8
- } from '../config.js';
9
-
10
- export async function configGetCommand(key: string): Promise<void> {
11
- try {
12
- const config = getConfig();
13
-
14
- switch (key) {
15
- case 'api-url':
16
- console.log(getApiUrl());
17
- break;
18
- case 'sync-enabled':
19
- console.log(isSyncEnabled());
20
- break;
21
- case 'all':
22
- console.log(chalk.bold('Configuration:'));
23
- console.log(` API URL: ${chalk.cyan(getApiUrl())}`);
24
- console.log(` Sync Enabled: ${chalk.cyan(isSyncEnabled())}`);
25
- console.log(` Has Token: ${chalk.cyan(config.apiToken ? 'yes' : 'no')}`);
26
- break;
27
- default:
28
- console.error(chalk.red(`Unknown config key: ${key}`));
29
- console.log('Available keys: api-url, sync-enabled, all');
30
- process.exit(1);
31
- }
32
- } catch (error) {
33
- console.error(chalk.red(`Error: ${(error as Error).message}`));
34
- process.exit(1);
35
- }
36
- }
37
-
38
- export async function configSetCommand(key: string, value: string): Promise<void> {
39
- try {
40
- switch (key) {
41
- case 'api-url':
42
- setApiUrl(value);
43
- console.log(chalk.green(`API URL set to: ${value}`));
44
- break;
45
- case 'sync-enabled':
46
- const enabled = value.toLowerCase() === 'true' || value === '1';
47
- setSyncEnabled(enabled);
48
- console.log(chalk.green(`Sync ${enabled ? 'enabled' : 'disabled'}`));
49
- break;
50
- default:
51
- console.error(chalk.red(`Unknown config key: ${key}`));
52
- console.log('Available keys: api-url, sync-enabled');
53
- process.exit(1);
54
- }
55
- } catch (error) {
56
- console.error(chalk.red(`Error: ${(error as Error).message}`));
57
- process.exit(1);
58
- }
59
- }
@@ -1,259 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as readline from 'readline';
4
- import chalk from 'chalk';
5
- import { isGitRepo, detectGitHubUsername } from '../utils/git.js';
6
- import { POST_COMMIT_HOOK } from '../templates/post-commit.js';
7
- import { getRepoInfo, setGitHubUsername, getGitHubUsername, setPrivateRepo, setPlayAsUsername } from '../config.js';
8
- import { authLoginCommand } from './auth.js';
9
-
10
- async function isRepoPublic(owner: string, repo: string): Promise<boolean | null> {
11
- try {
12
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
13
- headers: {
14
- 'Accept': 'application/vnd.github+json',
15
- 'X-GitHub-Api-Version': '2022-11-28',
16
- },
17
- });
18
-
19
- if (response.ok) {
20
- const data = await response.json() as { private: boolean };
21
- return data.private === false;
22
- }
23
-
24
- // 404 could mean private or doesn't exist
25
- return null;
26
- } catch (error) {
27
- // Network error or API unavailable
28
- return null;
29
- }
30
- }
31
-
32
- function askQuestion(question: string): Promise<string> {
33
- const rl = readline.createInterface({
34
- input: process.stdin,
35
- output: process.stdout
36
- });
37
-
38
- return new Promise((resolve) => {
39
- rl.question(question, (answer) => {
40
- rl.close();
41
- resolve(answer.trim().toLowerCase());
42
- });
43
- });
44
- }
45
-
46
- export async function initCommand(): Promise<void> {
47
- // Check if git repo
48
- if (!isGitRepo()) {
49
- console.error(chalk.red('Error: Not a git repository'));
50
- console.log(chalk.dim('Run this command from the root of a git repository'));
51
- process.exit(1);
52
- }
53
-
54
- // Check for GitHub remote
55
- const repoInfo = getRepoInfo();
56
-
57
- if (!repoInfo) {
58
- console.log();
59
- console.error(chalk.red('Error: No GitHub remote detected'));
60
- console.log();
61
- console.log(chalk.yellow('Git Slot Machine requires a GitHub repository.'));
62
- console.log();
63
- console.log(chalk.dim('Add a GitHub remote to this repo:'));
64
- console.log(chalk.cyan(' git remote add origin https://github.com/username/repo.git'));
65
- console.log();
66
- process.exit(1);
67
- }
68
-
69
- // Detect GitHub username (not repo owner)
70
- let githubUsername = getGitHubUsername();
71
-
72
- if (!githubUsername) {
73
- // Try to detect from git config
74
- githubUsername = detectGitHubUsername();
75
-
76
- if (githubUsername) {
77
- console.log(chalk.dim(`Detected GitHub username: ${githubUsername}`));
78
- } else {
79
- // Couldn't detect, prompt user
80
- console.log();
81
- console.log(chalk.yellow('GitHub username not detected'));
82
- console.log(chalk.dim('We need your GitHub username (not the repo owner) for the leaderboard'));
83
- githubUsername = await askQuestion(chalk.cyan('Enter your GitHub username: '));
84
-
85
- if (!githubUsername) {
86
- console.log(chalk.red('GitHub username is required'));
87
- process.exit(1);
88
- }
89
- }
90
-
91
- setGitHubUsername(githubUsername);
92
- }
93
-
94
- // Check if repository is public
95
- console.log(chalk.dim('Checking repository visibility...'));
96
- const isPublic = await isRepoPublic(repoInfo.owner, repoInfo.name);
97
-
98
- let usePrivacyMode = false;
99
-
100
- if (isPublic === false) {
101
- console.log(chalk.yellow('⚠️ Private repository detected'));
102
- console.log();
103
- console.log(chalk.cyan('Privacy Mode Available'));
104
- console.log(chalk.dim('You can still use Git Slot Machine with privacy mode enabled.'));
105
- console.log();
106
- console.log(chalk.yellow('What happens in privacy mode:'));
107
- console.log(chalk.dim(' • Repository name/org are NOT stored on the server'));
108
- console.log(chalk.dim(' • Sent as "private/private" to the API'));
109
- console.log(chalk.dim(' • Displayed as "*******/*******" on leaderboard'));
110
- console.log(chalk.dim(' • Your GitHub username is still public'));
111
- console.log(chalk.dim(' • All private repos share one balance'));
112
- console.log();
113
-
114
- const answer = await askQuestion(chalk.green('Enable privacy mode? (y/n): '));
115
-
116
- if (answer === 'y' || answer === 'yes') {
117
- usePrivacyMode = true;
118
- setPrivateRepo(true);
119
- console.log(chalk.green('✓ Privacy mode enabled'));
120
- console.log(chalk.dim('Repository details will never be sent to the server.'));
121
- } else {
122
- console.log();
123
- console.log(chalk.red('Cannot proceed without privacy mode for private repos.'));
124
- console.log(chalk.dim('Please make the repository public or enable privacy mode.'));
125
- process.exit(1);
126
- }
127
- } else if (isPublic === null) {
128
- console.log(chalk.yellow('⚠️ Could not verify repository visibility'));
129
- console.log();
130
-
131
- const answer = await askQuestion(chalk.green('Is this a private repository? (y/n): '));
132
-
133
- if (answer === 'y' || answer === 'yes') {
134
- usePrivacyMode = true;
135
- setPrivateRepo(true);
136
- console.log(chalk.green('✓ Privacy mode enabled'));
137
- console.log(chalk.dim('Repository details will never be sent to the server.'));
138
- } else {
139
- console.log(chalk.dim('Proceeding with public repository mode...'));
140
- }
141
- } else {
142
- console.log(chalk.green('✓ Public repository confirmed'));
143
- }
144
-
145
- const hookPath = path.join(process.cwd(), '.git', 'hooks', 'post-commit');
146
-
147
- // Check if hook already exists
148
- if (fs.existsSync(hookPath)) {
149
- console.log(chalk.yellow('⚠️ Post-commit hook already exists'));
150
- console.log(chalk.dim(`Location: ${hookPath}`));
151
- console.log();
152
- console.log(chalk.yellow('Skipping hook installation to avoid overwriting.'));
153
- console.log(chalk.dim('To use Git Slot Machine, manually add this to your existing hook:'));
154
- console.log(chalk.cyan(' git-slot-machine play'));
155
- console.log();
156
- } else {
157
- // Write the hook
158
- fs.writeFileSync(hookPath, POST_COMMIT_HOOK, { mode: 0o755 });
159
- console.log(chalk.green('✓ Post-commit hook installed'));
160
- console.log();
161
- }
162
-
163
- // Ask if they want to join the leaderboard
164
- const joinLeaderboard = await askQuestion(chalk.cyan('Join the global leaderboard? (Y/n): '));
165
- console.log();
166
-
167
- if (joinLeaderboard === 'n' || joinLeaderboard === 'no') {
168
- console.log(chalk.green('✓ Git Slot Machine is ready (local mode only)'));
169
- console.log(chalk.dim('Every commit will spin the slot machine locally.'));
170
- console.log();
171
- console.log(chalk.dim('To join the leaderboard later:'));
172
- console.log(chalk.cyan(' git-slot-machine login your-username'));
173
- console.log();
174
- } else {
175
- // Confirm detected username
176
- console.log(chalk.dim(`Detected GitHub username: ${githubUsername}`));
177
- const isCorrect = await askQuestion(chalk.cyan('Is this correct? (Y/n): '));
178
- console.log();
179
-
180
- if (isCorrect === 'n' || isCorrect === 'no') {
181
- githubUsername = await askQuestion(chalk.cyan('Enter your GitHub username: '));
182
- if (!githubUsername) {
183
- console.log(chalk.yellow('Skipping authentication - you can join later'));
184
- console.log(chalk.cyan(' git-slot-machine login your-username'));
185
- console.log();
186
- return;
187
- }
188
- setGitHubUsername(githubUsername);
189
- console.log();
190
- }
191
-
192
- // Ask if they want to play as org or personal username
193
- const repoOwner = repoInfo.owner;
194
-
195
- // Only ask if repo owner is different from personal username and not in privacy mode
196
- if (!usePrivacyMode && repoOwner.toLowerCase() !== githubUsername.toLowerCase()) {
197
- console.log(chalk.cyan('Who should get credit for commits in this repo?'));
198
- console.log();
199
- console.log(chalk.dim(` 1) ${githubUsername} (your personal account)`));
200
- console.log(chalk.dim(` 2) ${repoOwner} (this repo's organization)`));
201
- console.log();
202
-
203
- const choice = await askQuestion(chalk.cyan('Choose (1 or 2): '));
204
- console.log();
205
-
206
- if (choice === '2') {
207
- // Play as org
208
- setPlayAsUsername(repoOwner);
209
- console.log(chalk.green(`✓ Commits in this repo will be credited to ${repoOwner}`));
210
- console.log();
211
-
212
- // Update githubUsername for authentication
213
- githubUsername = repoOwner;
214
- } else {
215
- // Play as personal username (default)
216
- console.log(chalk.green(`✓ Commits in this repo will be credited to ${githubUsername}`));
217
- console.log();
218
- }
219
- }
220
-
221
- // Authenticate
222
- console.log(chalk.dim('Authenticating...'));
223
- try {
224
- await authLoginCommand(githubUsername);
225
- console.log(chalk.green('✓ You\'re on the leaderboard!'));
226
- console.log(chalk.dim('View it at: https://gitslotmachine.com'));
227
- console.log();
228
- } catch (error) {
229
- console.log(chalk.yellow('⚠️ Authentication failed'));
230
- console.log(chalk.dim('Your commits will work locally, but won\'t appear on the leaderboard'));
231
- console.log(chalk.dim(`Try again: git-slot-machine login ${githubUsername}`));
232
- console.log();
233
- }
234
- }
235
-
236
- console.log(chalk.cyan('Try it out:'));
237
- console.log(chalk.dim(' git commit --allow-empty -m "test"'));
238
- console.log();
239
- console.log(chalk.yellow('What gets sent to the server:'));
240
-
241
- if (usePrivacyMode) {
242
- // Privacy mode - show what is NOT sent
243
- console.log(chalk.green(' ✓ Commit hash (7 and 40 character versions)'));
244
- console.log(chalk.red(' ✗ Repository URL, owner, and name'));
245
- console.log(chalk.dim(' (Sent as "private/private" instead)'));
246
- console.log(chalk.green(' ✓ GitHub username'));
247
- console.log(chalk.green(' ✓ Pattern type, payout, and balance'));
248
- } else {
249
- // Public mode - everything is sent
250
- console.log(chalk.green(' ✓ Commit hash (7 and 40 character versions)'));
251
- console.log(chalk.green(' ✓ Repository URL, owner, and name'));
252
- console.log(chalk.green(' ✓ GitHub username'));
253
- console.log(chalk.green(' ✓ Pattern type, payout, and balance'));
254
- }
255
-
256
- console.log();
257
- console.log(chalk.dim('You can disable API sync anytime:'));
258
- console.log(chalk.dim(' git-slot-machine sync:disable'));
259
- }
@@ -1,196 +0,0 @@
1
- import { detectPattern, PatternType } from '../patterns.js';
2
- import { animateSlotMachine, animateSmallMode } from '../animation/slotMachine.js';
3
- import { getBalance, updateBalance, setBalance } from '../balance.js';
4
- import { sendPlayToAPI } from '../api.js';
5
- import { getRepoInfo, getGitHubUsername } from '../config.js';
6
- import { detectAmendGrinding, getAmendWarningMessage } from '../utils/amendDetector.js';
7
- import { checkSecret } from '../secrets.js';
8
- import chalk from 'chalk';
9
-
10
- function floodTerminal(emoji: string): void {
11
- const cols = process.stdout.columns || 80;
12
- const rows = 5;
13
- const line = emoji.repeat(Math.floor(cols / 2));
14
- for (let i = 0; i < rows; i++) {
15
- console.log(line);
16
- }
17
- }
18
-
19
- interface PlayOptions {
20
- small?: boolean;
21
- fullHash?: string; // Optional full hash for CLI integration
22
- }
23
-
24
- export async function playCommand(hash: string, options: PlayOptions): Promise<void> {
25
- try {
26
- // Get balance before playing
27
- const balanceBefore = getBalance();
28
-
29
- // Check for secret combos first
30
- const secret = checkSecret(hash);
31
-
32
- // If secret with emoji flood, do it before anything else
33
- if (secret?.flood && secret.emoji) {
34
- if (options.small) {
35
- // Small mode - inline emoji burst
36
- process.stdout.write(secret.emoji.repeat(10) + ' ');
37
- } else {
38
- console.clear();
39
- floodTerminal(secret.emoji);
40
- await new Promise(resolve => setTimeout(resolve, 1500));
41
- }
42
- }
43
-
44
- // Detect pattern (or use secret)
45
- const result = secret
46
- ? {
47
- type: PatternType.SECRET,
48
- name: secret.name,
49
- payout: secret.payout,
50
- description: '???',
51
- highlightIndices: [0, 1, 2, 3, 4, 5, 6],
52
- }
53
- : detectPattern(hash);
54
-
55
- // Animate based on mode
56
- const config = {
57
- finalHash: hash.toLowerCase(),
58
- small: options.small || false,
59
- patternResult: result
60
- };
61
-
62
- if (options.small) {
63
- await animateSmallMode(config);
64
- } else {
65
- await animateSlotMachine(config);
66
- }
67
-
68
- // Show result
69
- if (!options.small) {
70
- console.log();
71
-
72
- // Center the text below the box (box width is 41 chars)
73
- const boxWidth = 41;
74
-
75
- if (result.payout > 0) {
76
- const resultText = `${result.name}!`;
77
- const resultPadding = Math.floor((boxWidth - resultText.length) / 2);
78
- console.log(' '.repeat(resultPadding) + chalk.cyan.bold(resultText));
79
-
80
- const payoutText = `+${result.payout} points`;
81
- const payoutPadding = Math.floor((boxWidth - payoutText.length) / 2);
82
- console.log(' '.repeat(payoutPadding) + chalk.white.bold(payoutText));
83
- } else {
84
- const noWinText = 'No win';
85
- const noWinPadding = Math.floor((boxWidth - noWinText.length) / 2);
86
- console.log(' '.repeat(noWinPadding) + chalk.red.bold(noWinText));
87
-
88
- const lossText = '-10 points';
89
- const lossPadding = Math.floor((boxWidth - lossText.length) / 2);
90
- console.log(' '.repeat(lossPadding) + chalk.white.bold(lossText));
91
- }
92
-
93
- console.log();
94
- const descText = result.description;
95
- const descPadding = Math.floor((boxWidth - descText.length) / 2);
96
- console.log(' '.repeat(descPadding) + chalk.dim(descText));
97
- }
98
-
99
- // Validate GitHub remote for API sync
100
- const repoInfo = getRepoInfo();
101
- const githubUsername = getGitHubUsername();
102
-
103
- if (!repoInfo && githubUsername) {
104
- console.log();
105
- console.log(chalk.yellow.bold('⚠ Warning: No GitHub remote detected'));
106
- console.log(chalk.dim('This repo will not sync to the leaderboard.'));
107
- console.log(chalk.dim('To sync, add a GitHub remote:'));
108
- console.log(chalk.cyan(' git remote add origin https://github.com/username/repo.git'));
109
- console.log();
110
- }
111
-
112
- // Update balance locally
113
- let newBalance = updateBalance(hash.toLowerCase(), result.payout);
114
-
115
- // Detect potential hash grinding
116
- const amendDetection = detectAmendGrinding();
117
- const warningMessage = getAmendWarningMessage(amendDetection);
118
-
119
- // Send to API and sync balance with server
120
- let shareUrl: string | undefined;
121
-
122
- if (repoInfo && githubUsername) {
123
- const playData: any = {
124
- commit_hash: hash.toLowerCase(),
125
- pattern_type: result.type,
126
- pattern_name: result.name,
127
- payout: result.payout,
128
- wager: 10,
129
- balance_before: balanceBefore,
130
- balance_after: newBalance,
131
- repo_url: repoInfo.url,
132
- github_username: githubUsername,
133
- repo_owner: repoInfo.owner,
134
- repo_name: repoInfo.name,
135
- suspicious: amendDetection.suspiciousActivity,
136
- amend_count: amendDetection.recentAmendCount,
137
- };
138
-
139
- // Only send full hash if we have one (not in test mode)
140
- if (options.fullHash && options.fullHash.length === 40) {
141
- playData.commit_full_hash = options.fullHash;
142
- }
143
-
144
- try {
145
- const apiResponse = await sendPlayToAPI(playData);
146
- // Sync local balance to match server's balance
147
- if (apiResponse && apiResponse.balance !== undefined) {
148
- setBalance(apiResponse.balance);
149
- newBalance = apiResponse.balance;
150
- shareUrl = apiResponse.share_url;
151
- }
152
- } catch (error) {
153
- // Silently fail - local play already succeeded
154
- }
155
- }
156
-
157
- // Show result and balance
158
- if (options.small) {
159
- // Small mode - everything on one line (animateSmallMode already wrote the hash without newline)
160
- if (result.payout > 0) {
161
- console.log(chalk.dim(' • ') + chalk.cyan.bold(`${result.name} +${result.payout}`) + chalk.dim(' • ') + chalk.white(`Balance: ${chalk.green.bold(newBalance)}`));
162
- } else {
163
- console.log(chalk.dim(' • ') + chalk.red('No win -10') + chalk.dim(' • ') + chalk.white(`Balance: ${newBalance >= 0 ? chalk.green.bold(newBalance) : chalk.red.bold(newBalance)}`));
164
- }
165
- } else {
166
- console.log();
167
- const boxWidth = 41;
168
- // Note: we can't measure the exact length with color codes, so estimate based on text content
169
- const balanceText = `Balance: ${newBalance} points`;
170
- const balancePadding = Math.floor((boxWidth - balanceText.length) / 2);
171
- console.log(' '.repeat(balancePadding) + chalk.white.bold(`Balance: ${newBalance >= 0 ? chalk.green.bold(newBalance) : chalk.red.bold(newBalance)} points`));
172
- }
173
-
174
- // Show share URL for wins
175
- if (shareUrl && result.payout > 0 && !options.small) {
176
- console.log();
177
- const boxWidth = 41;
178
- const shareText = 'Share your win:';
179
- const sharePadding = Math.floor((boxWidth - shareText.length) / 2);
180
- console.log(' '.repeat(sharePadding) + chalk.dim(shareText));
181
-
182
- const urlPadding = Math.floor((boxWidth - shareUrl.length) / 2);
183
- console.log(' '.repeat(urlPadding) + chalk.green.underline(shareUrl));
184
- }
185
-
186
- // Show warning if suspicious activity detected
187
- if (warningMessage && !options.small) {
188
- console.log();
189
- console.log(chalk.yellow(warningMessage));
190
- }
191
-
192
- } catch (error) {
193
- console.error(chalk.red(`Error: ${(error as Error).message}`));
194
- process.exit(1);
195
- }
196
- }
@@ -1,17 +0,0 @@
1
- import { getCurrentCommitHash, getCurrentCommitFullHash } from '../utils/git.js';
2
- import { playCommand } from './play.js';
3
-
4
- interface SpinOptions {
5
- small?: boolean;
6
- }
7
-
8
- export async function spinCommand(options: SpinOptions): Promise<void> {
9
- try {
10
- const hash = getCurrentCommitHash();
11
- const fullHash = getCurrentCommitFullHash();
12
- await playCommand(hash, { ...options, fullHash });
13
- } catch (error) {
14
- console.error(`Error: ${(error as Error).message}`);
15
- process.exit(1);
16
- }
17
- }