git-slot-machine 1.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +313 -0
  3. package/dist/animation/slotMachine.d.ts +9 -0
  4. package/dist/animation/slotMachine.d.ts.map +1 -0
  5. package/dist/animation/slotMachine.js +139 -0
  6. package/dist/animation/slotMachine.js.map +1 -0
  7. package/dist/api.d.ts +40 -0
  8. package/dist/api.d.ts.map +1 -0
  9. package/dist/api.js +154 -0
  10. package/dist/api.js.map +1 -0
  11. package/dist/balance.d.ts +11 -0
  12. package/dist/balance.d.ts.map +1 -0
  13. package/dist/balance.js +131 -0
  14. package/dist/balance.js.map +1 -0
  15. package/dist/commands/auth.d.ts +4 -0
  16. package/dist/commands/auth.d.ts.map +1 -0
  17. package/dist/commands/auth.js +91 -0
  18. package/dist/commands/auth.js.map +1 -0
  19. package/dist/commands/balance.d.ts +2 -0
  20. package/dist/commands/balance.d.ts.map +1 -0
  21. package/dist/commands/balance.js +32 -0
  22. package/dist/commands/balance.js.map +1 -0
  23. package/dist/commands/config.d.ts +3 -0
  24. package/dist/commands/config.d.ts.map +1 -0
  25. package/dist/commands/config.js +60 -0
  26. package/dist/commands/config.js.map +1 -0
  27. package/dist/commands/init.d.ts +2 -0
  28. package/dist/commands/init.d.ts.map +1 -0
  29. package/dist/commands/init.js +148 -0
  30. package/dist/commands/init.js.map +1 -0
  31. package/dist/commands/play.d.ts +7 -0
  32. package/dist/commands/play.d.ts.map +1 -0
  33. package/dist/commands/play.js +137 -0
  34. package/dist/commands/play.js.map +1 -0
  35. package/dist/commands/spin.d.ts +6 -0
  36. package/dist/commands/spin.d.ts.map +1 -0
  37. package/dist/commands/spin.js +17 -0
  38. package/dist/commands/spin.js.map +1 -0
  39. package/dist/commands/sync.d.ts +2 -0
  40. package/dist/commands/sync.d.ts.map +1 -0
  41. package/dist/commands/sync.js +52 -0
  42. package/dist/commands/sync.js.map +1 -0
  43. package/dist/commands/test.d.ts +6 -0
  44. package/dist/commands/test.d.ts.map +1 -0
  45. package/dist/commands/test.js +17 -0
  46. package/dist/commands/test.js.map +1 -0
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/config.js +144 -0
  50. package/dist/config.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +100 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/patterns.d.ts +27 -0
  56. package/dist/patterns.d.ts.map +1 -0
  57. package/dist/patterns.js +266 -0
  58. package/dist/patterns.js.map +1 -0
  59. package/dist/templates/post-commit.d.ts +2 -0
  60. package/dist/templates/post-commit.d.ts.map +1 -0
  61. package/dist/templates/post-commit.js +19 -0
  62. package/dist/templates/post-commit.js.map +1 -0
  63. package/dist/utils/git.d.ts +4 -0
  64. package/dist/utils/git.d.ts.map +1 -0
  65. package/dist/utils/git.js +42 -0
  66. package/dist/utils/git.js.map +1 -0
  67. package/jest.config.js +12 -0
  68. package/package.json +50 -0
  69. package/src/animation/slotMachine.ts +159 -0
  70. package/src/api.ts +203 -0
  71. package/src/balance.ts +118 -0
  72. package/src/commands/auth.ts +92 -0
  73. package/src/commands/balance.ts +28 -0
  74. package/src/commands/config.ts +59 -0
  75. package/src/commands/init.ts +121 -0
  76. package/src/commands/play.ts +150 -0
  77. package/src/commands/spin.ts +17 -0
  78. package/src/commands/sync.ts +49 -0
  79. package/src/commands/test.ts +19 -0
  80. package/src/config.ts +154 -0
  81. package/src/index.ts +114 -0
  82. package/src/patterns.test.ts +44 -0
  83. package/src/patterns.ts +292 -0
  84. package/src/templates/post-commit.ts +15 -0
  85. package/src/utils/git.ts +38 -0
  86. package/test.txt +2 -0
  87. package/tsconfig.json +20 -0
@@ -0,0 +1,121 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import chalk from 'chalk';
4
+ import { isGitRepo } from '../utils/git';
5
+ import { POST_COMMIT_HOOK } from '../templates/post-commit';
6
+ import { getRepoInfo, setGitHubUsername, getGitHubUsername } from '../config';
7
+
8
+ async function isRepoPublic(owner: string, repo: string): Promise<boolean | null> {
9
+ try {
10
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
11
+ headers: {
12
+ 'Accept': 'application/vnd.github+json',
13
+ 'X-GitHub-Api-Version': '2022-11-28',
14
+ },
15
+ });
16
+
17
+ if (response.ok) {
18
+ const data = await response.json() as { private: boolean };
19
+ return data.private === false;
20
+ }
21
+
22
+ // 404 could mean private or doesn't exist
23
+ return null;
24
+ } catch (error) {
25
+ // Network error or API unavailable
26
+ return null;
27
+ }
28
+ }
29
+
30
+ export async function initCommand(): Promise<void> {
31
+ // Check if git repo
32
+ if (!isGitRepo()) {
33
+ console.error(chalk.red('Error: Not a git repository'));
34
+ console.log(chalk.dim('Run this command from the root of a git repository'));
35
+ process.exit(1);
36
+ }
37
+
38
+ // Extract and save GitHub username from remote URL (do this first)
39
+ const repoInfo = getRepoInfo();
40
+
41
+ if (!repoInfo) {
42
+ console.log();
43
+ console.error(chalk.red('Error: No GitHub remote detected'));
44
+ console.log();
45
+ console.log(chalk.yellow('Git Slot Machine requires a public GitHub repository.'));
46
+ console.log();
47
+ console.log(chalk.dim('Add a GitHub remote to this repo:'));
48
+ console.log(chalk.cyan(' git remote add origin https://github.com/username/repo.git'));
49
+ console.log();
50
+ console.log(chalk.dim('This prevents farming points in private/local repos.'));
51
+ console.log(chalk.dim('Your commits must be publicly verifiable.'));
52
+ process.exit(1);
53
+ }
54
+
55
+ const existingUsername = getGitHubUsername();
56
+ if (!existingUsername) {
57
+ setGitHubUsername(repoInfo.owner);
58
+ console.log(chalk.dim(`Detected GitHub username: ${repoInfo.owner}`));
59
+ }
60
+
61
+ // Check if repository is public
62
+ console.log(chalk.dim('Checking repository visibility...'));
63
+ const isPublic = await isRepoPublic(repoInfo.owner, repoInfo.name);
64
+
65
+ if (isPublic === false) {
66
+ console.log();
67
+ console.error(chalk.red('Error: Private repository detected'));
68
+ console.log();
69
+ console.log(chalk.yellow('Git Slot Machine only supports public repositories.'));
70
+ console.log();
71
+ console.log(chalk.dim('Why? Your commit hashes would be visible on the public'));
72
+ console.log(chalk.dim('leaderboard, which could expose information about your'));
73
+ console.log(chalk.dim('private repository.'));
74
+ console.log();
75
+ console.log(chalk.dim('Please use git-slot-machine with a public repository.'));
76
+ process.exit(1);
77
+ }
78
+
79
+ if (isPublic === null) {
80
+ console.log(chalk.yellow('⚠️ Could not verify repository visibility'));
81
+ console.log(chalk.dim('Proceeding with installation...'));
82
+ } else {
83
+ console.log(chalk.green('✓ Public repository confirmed'));
84
+ }
85
+
86
+ const hookPath = path.join(process.cwd(), '.git', 'hooks', 'post-commit');
87
+
88
+ // Check if hook already exists
89
+ if (fs.existsSync(hookPath)) {
90
+ console.log(chalk.yellow('Warning: post-commit hook already exists'));
91
+ console.log(chalk.dim(`Location: ${hookPath}`));
92
+
93
+ // TODO: Could add merge logic or backup existing hook
94
+ console.log(chalk.red('Aborting to avoid overwriting existing hook'));
95
+ console.log(chalk.dim('Manually add git-slot-machine to your existing hook'));
96
+ process.exit(1);
97
+ }
98
+
99
+ // Write the hook
100
+ fs.writeFileSync(hookPath, POST_COMMIT_HOOK, { mode: 0o755 });
101
+
102
+ console.log(chalk.green('✓ Post-commit hook installed'));
103
+ console.log();
104
+ console.log(chalk.cyan('Git Slot Machine is ready!'));
105
+ console.log(chalk.dim('Every commit will now spin the slot machine.'));
106
+ console.log();
107
+ console.log('Try it out:');
108
+ console.log(chalk.dim(' git commit --allow-empty -m "test"'));
109
+ console.log();
110
+ console.log(chalk.cyan('Optional: Sync with the API'));
111
+ console.log(chalk.dim(` git-slot-machine auth login ${repoInfo?.owner || 'your-github-username'}`));
112
+ console.log();
113
+ console.log(chalk.yellow('What gets sent to the server:'));
114
+ console.log(chalk.dim(' • Commit hash (7 and 40 character versions)'));
115
+ console.log(chalk.dim(' • Repository URL, owner, and name'));
116
+ console.log(chalk.dim(' • GitHub username'));
117
+ console.log(chalk.dim(' • Pattern type, payout, and balance'));
118
+ console.log();
119
+ console.log(chalk.dim('You can disable API sync anytime:'));
120
+ console.log(chalk.dim(' git-slot-machine config set sync-enabled false'));
121
+ }
@@ -0,0 +1,150 @@
1
+ import { detectPattern } from '../patterns';
2
+ import { animateSlotMachine, animateSmallMode } from '../animation/slotMachine';
3
+ import { getBalance, updateBalance, setBalance } from '../balance';
4
+ import { sendPlayToAPI } from '../api';
5
+ import { getRepoInfo, getGitHubUsername } from '../config';
6
+ import chalk from 'chalk';
7
+
8
+ interface PlayOptions {
9
+ small?: boolean;
10
+ fullHash?: string; // Optional full hash for CLI integration
11
+ }
12
+
13
+ export async function playCommand(hash: string, options: PlayOptions): Promise<void> {
14
+ try {
15
+ // Get balance before playing
16
+ const balanceBefore = getBalance();
17
+
18
+ // Detect pattern
19
+ const result = detectPattern(hash);
20
+
21
+ // Animate based on mode
22
+ const config = {
23
+ finalHash: hash.toLowerCase(),
24
+ small: options.small || false,
25
+ patternResult: result
26
+ };
27
+
28
+ if (options.small) {
29
+ await animateSmallMode(config);
30
+ } else {
31
+ await animateSlotMachine(config);
32
+ }
33
+
34
+ // Show result
35
+ if (!options.small) {
36
+ console.log();
37
+
38
+ // Center the text below the box (box width is 41 chars)
39
+ const boxWidth = 41;
40
+
41
+ if (result.payout > 0) {
42
+ const resultText = `${result.name}!`;
43
+ const resultPadding = Math.floor((boxWidth - resultText.length) / 2);
44
+ console.log(' '.repeat(resultPadding) + chalk.cyan.bold(resultText));
45
+
46
+ const payoutText = `+${result.payout} points`;
47
+ const payoutPadding = Math.floor((boxWidth - payoutText.length) / 2);
48
+ console.log(' '.repeat(payoutPadding) + chalk.white.bold(payoutText));
49
+ } else {
50
+ const noWinText = 'No win';
51
+ const noWinPadding = Math.floor((boxWidth - noWinText.length) / 2);
52
+ console.log(' '.repeat(noWinPadding) + chalk.red.bold(noWinText));
53
+
54
+ const lossText = '-10 points';
55
+ const lossPadding = Math.floor((boxWidth - lossText.length) / 2);
56
+ console.log(' '.repeat(lossPadding) + chalk.white.bold(lossText));
57
+ }
58
+
59
+ console.log();
60
+ const descText = result.description;
61
+ const descPadding = Math.floor((boxWidth - descText.length) / 2);
62
+ console.log(' '.repeat(descPadding) + chalk.dim(descText));
63
+ }
64
+
65
+ // Validate GitHub remote for API sync
66
+ const repoInfo = getRepoInfo();
67
+ const githubUsername = getGitHubUsername();
68
+
69
+ if (!repoInfo && githubUsername) {
70
+ console.log();
71
+ console.log(chalk.yellow.bold('⚠ Warning: No GitHub remote detected'));
72
+ console.log(chalk.dim('This repo will not sync to the leaderboard.'));
73
+ console.log(chalk.dim('To sync, add a GitHub remote:'));
74
+ console.log(chalk.cyan(' git remote add origin https://github.com/username/repo.git'));
75
+ console.log();
76
+ }
77
+
78
+ // Update balance locally
79
+ let newBalance = updateBalance(hash.toLowerCase(), result.payout);
80
+
81
+ // Send to API and sync balance with server
82
+ let shareUrl: string | undefined;
83
+
84
+ if (repoInfo && githubUsername) {
85
+ const playData: any = {
86
+ commit_hash: hash.toLowerCase(),
87
+ pattern_type: result.type,
88
+ pattern_name: result.name,
89
+ payout: result.payout,
90
+ wager: 10,
91
+ balance_before: balanceBefore,
92
+ balance_after: newBalance,
93
+ repo_url: repoInfo.url,
94
+ github_username: githubUsername,
95
+ repo_owner: repoInfo.owner,
96
+ repo_name: repoInfo.name,
97
+ };
98
+
99
+ // Only send full hash if we have one (not in test mode)
100
+ if (options.fullHash && options.fullHash.length === 40) {
101
+ playData.commit_full_hash = options.fullHash;
102
+ }
103
+
104
+ try {
105
+ const apiResponse = await sendPlayToAPI(playData);
106
+ // Sync local balance to match server's balance
107
+ if (apiResponse && apiResponse.balance !== undefined) {
108
+ setBalance(apiResponse.balance);
109
+ newBalance = apiResponse.balance;
110
+ shareUrl = apiResponse.share_url;
111
+ }
112
+ } catch (error) {
113
+ // Silently fail - local play already succeeded
114
+ }
115
+ }
116
+
117
+ // Show result and balance
118
+ if (options.small) {
119
+ // Small mode - everything on one line (animateSmallMode already wrote the hash without newline)
120
+ if (result.payout > 0) {
121
+ console.log(chalk.dim(' • ') + chalk.cyan.bold(`${result.name} +${result.payout}`) + chalk.dim(' • ') + chalk.white(`Balance: ${chalk.green.bold(newBalance)}`));
122
+ } else {
123
+ console.log(chalk.dim(' • ') + chalk.red('No win -10') + chalk.dim(' • ') + chalk.white(`Balance: ${newBalance >= 0 ? chalk.green.bold(newBalance) : chalk.red.bold(newBalance)}`));
124
+ }
125
+ } else {
126
+ console.log();
127
+ const boxWidth = 41;
128
+ // Note: we can't measure the exact length with color codes, so estimate based on text content
129
+ const balanceText = `Balance: ${newBalance} points`;
130
+ const balancePadding = Math.floor((boxWidth - balanceText.length) / 2);
131
+ console.log(' '.repeat(balancePadding) + chalk.white.bold(`Balance: ${newBalance >= 0 ? chalk.green.bold(newBalance) : chalk.red.bold(newBalance)} points`));
132
+ }
133
+
134
+ // Show share URL for wins
135
+ if (shareUrl && result.payout > 0 && !options.small) {
136
+ console.log();
137
+ const boxWidth = 41;
138
+ const shareText = 'Share your win:';
139
+ const sharePadding = Math.floor((boxWidth - shareText.length) / 2);
140
+ console.log(' '.repeat(sharePadding) + chalk.dim(shareText));
141
+
142
+ const urlPadding = Math.floor((boxWidth - shareUrl.length) / 2);
143
+ console.log(' '.repeat(urlPadding) + chalk.green.underline(shareUrl));
144
+ }
145
+
146
+ } catch (error) {
147
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
148
+ process.exit(1);
149
+ }
150
+ }
@@ -0,0 +1,17 @@
1
+ import { getCurrentCommitHash, getCurrentCommitFullHash } from '../utils/git';
2
+ import { playCommand } from './play';
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
+ }
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { getBalance as getApiBalance } from '../api';
3
+ import { getBalance as getLocalBalance } from '../balance';
4
+
5
+ export async function syncCommand(): Promise<void> {
6
+ try {
7
+ console.log(chalk.dim('Syncing with API...'));
8
+
9
+ const apiBalance = await getApiBalance();
10
+
11
+ if (!apiBalance) {
12
+ console.log(chalk.yellow('Unable to sync with API.'));
13
+ console.log(chalk.dim('Make sure you are authenticated and online.'));
14
+ return;
15
+ }
16
+
17
+ const localBalance = getLocalBalance();
18
+
19
+ console.log();
20
+ console.log(chalk.bold('Local Balance:'));
21
+ console.log(` Balance: ${chalk.green(localBalance)} points`);
22
+ console.log();
23
+ console.log(chalk.bold('API Balance:'));
24
+ console.log(` Balance: ${chalk.green(apiBalance.balance)} points`);
25
+ console.log(` Total Commits: ${chalk.cyan(apiBalance.total_commits)}`);
26
+ console.log(` Total Winnings: ${chalk.cyan(apiBalance.total_winnings)}`);
27
+
28
+ // Show biggest win with pattern and hash
29
+ if (apiBalance.biggest_win > 0) {
30
+ const winText = `${chalk.cyan(apiBalance.biggest_win)} points`;
31
+ const patternText = apiBalance.biggest_win_pattern ? ` (${chalk.yellow(apiBalance.biggest_win_pattern)})` : '';
32
+ const hashText = apiBalance.biggest_win_hash ? chalk.dim(` - ${apiBalance.biggest_win_hash}`) : '';
33
+ console.log(` Biggest Win: ${winText}${patternText}${hashText}`);
34
+ } else {
35
+ console.log(` Biggest Win: ${chalk.cyan(apiBalance.biggest_win)}`);
36
+ }
37
+ console.log();
38
+
39
+ if (localBalance !== apiBalance.balance) {
40
+ console.log(chalk.yellow('Warning: Local and API balances differ.'));
41
+ console.log(chalk.dim('This is normal if you play offline or in multiple repos.'));
42
+ } else {
43
+ console.log(chalk.green('Balances are in sync!'));
44
+ }
45
+ } catch (error) {
46
+ console.error(chalk.red(`Error: ${(error as Error).message}`));
47
+ process.exit(1);
48
+ }
49
+ }
@@ -0,0 +1,19 @@
1
+ import { playCommand } from './play';
2
+
3
+ interface TestOptions {
4
+ small?: boolean;
5
+ }
6
+
7
+ function generateRandomHash(): string {
8
+ const hexChars = '0123456789abcdef';
9
+ let hash = '';
10
+ for (let i = 0; i < 7; i++) {
11
+ hash += hexChars[Math.floor(Math.random() * hexChars.length)];
12
+ }
13
+ return hash;
14
+ }
15
+
16
+ export async function testCommand(options: TestOptions): Promise<void> {
17
+ const randomHash = generateRandomHash();
18
+ await playCommand(randomHash, options);
19
+ }
package/src/config.ts ADDED
@@ -0,0 +1,154 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { execSync } from 'child_process';
5
+
6
+ interface Config {
7
+ githubUsername?: string;
8
+ apiUrl?: string;
9
+ apiToken?: string;
10
+ syncEnabled?: boolean;
11
+ }
12
+
13
+ // Get repo-specific config path
14
+ function getRepoConfigPath(): string {
15
+ return path.join(process.cwd(), '.git', 'slot-machine-config.json');
16
+ }
17
+
18
+ // Get global config path
19
+ function getGlobalConfigPath(): string {
20
+ const homeDir = os.homedir();
21
+ const configDir = path.join(homeDir, '.git-slot-machine');
22
+
23
+ // Ensure config directory exists
24
+ if (!fs.existsSync(configDir)) {
25
+ fs.mkdirSync(configDir, { recursive: true });
26
+ }
27
+
28
+ return path.join(configDir, 'config.json');
29
+ }
30
+
31
+ // Get merged config (repo-specific overrides global)
32
+ export function getConfig(): Config {
33
+ const globalConfig = getGlobalConfig();
34
+ const repoConfig = getRepoConfig();
35
+
36
+ return { ...globalConfig, ...repoConfig };
37
+ }
38
+
39
+ // Get only repo-specific config
40
+ export function getRepoConfig(): Config {
41
+ const configPath = getRepoConfigPath();
42
+
43
+ if (!fs.existsSync(configPath)) {
44
+ return {};
45
+ }
46
+
47
+ try {
48
+ const content = fs.readFileSync(configPath, 'utf-8');
49
+ return JSON.parse(content);
50
+ } catch {
51
+ return {};
52
+ }
53
+ }
54
+
55
+ // Get only global config
56
+ export function getGlobalConfig(): Config {
57
+ const configPath = getGlobalConfigPath();
58
+
59
+ if (!fs.existsSync(configPath)) {
60
+ return {};
61
+ }
62
+
63
+ try {
64
+ const content = fs.readFileSync(configPath, 'utf-8');
65
+ return JSON.parse(content);
66
+ } catch {
67
+ return {};
68
+ }
69
+ }
70
+
71
+ // Save repo-specific config
72
+ export function saveRepoConfig(config: Config): void {
73
+ const configPath = getRepoConfigPath();
74
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
75
+ }
76
+
77
+ // Save global config
78
+ export function saveGlobalConfig(config: Config): void {
79
+ const configPath = getGlobalConfigPath();
80
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
81
+ }
82
+
83
+ export function getGitHubUsername(): string | null {
84
+ const config = getConfig();
85
+ return config.githubUsername || null;
86
+ }
87
+
88
+ export function setGitHubUsername(username: string): void {
89
+ const config = getRepoConfig();
90
+ config.githubUsername = username;
91
+ saveRepoConfig(config);
92
+ }
93
+
94
+ export function getApiUrl(): string {
95
+ const config = getConfig();
96
+ return config.apiUrl || process.env.GIT_SLOT_MACHINE_API_URL || 'https://gitslotmachine.com/api';
97
+ }
98
+
99
+ export function setApiUrl(url: string): void {
100
+ const config = getGlobalConfig();
101
+ config.apiUrl = url;
102
+ saveGlobalConfig(config);
103
+ }
104
+
105
+ export function getApiToken(): string | null {
106
+ const config = getConfig();
107
+ return config.apiToken || null;
108
+ }
109
+
110
+ export function setApiToken(token: string): void {
111
+ const config = getGlobalConfig();
112
+ config.apiToken = token;
113
+ saveGlobalConfig(config);
114
+ }
115
+
116
+ export function clearApiToken(): void {
117
+ const config = getGlobalConfig();
118
+ delete config.apiToken;
119
+ saveGlobalConfig(config);
120
+ }
121
+
122
+ export function isSyncEnabled(): boolean {
123
+ const config = getConfig();
124
+ return config.syncEnabled !== false; // Default to true
125
+ }
126
+
127
+ export function setSyncEnabled(enabled: boolean): void {
128
+ const config = getGlobalConfig();
129
+ config.syncEnabled = enabled;
130
+ saveGlobalConfig(config);
131
+ }
132
+
133
+ export function getRepoInfo(): { owner: string; name: string; url: string } | null {
134
+ try {
135
+ const remoteUrl = execSync('git config --get remote.origin.url', { encoding: 'utf-8' }).trim();
136
+
137
+ // Parse GitHub URL (supports both HTTPS and SSH)
138
+ const match = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
139
+
140
+ if (match) {
141
+ const owner = match[1];
142
+ const name = match[2];
143
+ return {
144
+ owner,
145
+ name,
146
+ url: `https://github.com/${owner}/${name}`,
147
+ };
148
+ }
149
+
150
+ return null;
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
package/src/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { playCommand } from './commands/play';
5
+ import { spinCommand } from './commands/spin';
6
+ import { initCommand } from './commands/init';
7
+ import { balanceCommand } from './commands/balance';
8
+ import { testCommand } from './commands/test';
9
+ import { authLoginCommand, authLogoutCommand, authStatusCommand } from './commands/auth';
10
+ import { syncCommand } from './commands/sync';
11
+ import { configGetCommand, configSetCommand } from './commands/config';
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('git-slot-machine')
17
+ .description('Git commit hash slot machine')
18
+ .version('0.1.0');
19
+
20
+ program
21
+ .command('play')
22
+ .description('Play the slot machine with a git hash')
23
+ .argument('<hash>', '7-character git commit hash')
24
+ .option('-s, --small', 'Single line output')
25
+ .action(async (hash: string, options: any) => {
26
+ await playCommand(hash, options);
27
+ });
28
+
29
+ program
30
+ .command('spin')
31
+ .description('Play with the current git commit hash')
32
+ .option('-s, --small', 'Single line output')
33
+ .action(async (options: any) => {
34
+ await spinCommand(options);
35
+ });
36
+
37
+ program
38
+ .command('init')
39
+ .description('Install post-commit hook in current repository')
40
+ .action(async () => {
41
+ await initCommand();
42
+ });
43
+
44
+ program
45
+ .command('balance')
46
+ .description('Show current repository balance and stats')
47
+ .action(balanceCommand);
48
+
49
+ program
50
+ .command('test')
51
+ .description('Play with a random 7-character hash')
52
+ .option('-s, --small', 'Single line output')
53
+ .action(async (options: any) => {
54
+ await testCommand(options);
55
+ });
56
+
57
+ // Auth commands
58
+ const auth = program
59
+ .command('auth')
60
+ .description('Manage API authentication');
61
+
62
+ auth
63
+ .command('login')
64
+ .description('Login with GitHub username')
65
+ .argument('<github-username>', 'Your GitHub username')
66
+ .action(async (githubUsername: string) => {
67
+ await authLoginCommand(githubUsername);
68
+ });
69
+
70
+ auth
71
+ .command('logout')
72
+ .description('Logout and clear API token')
73
+ .action(async () => {
74
+ await authLogoutCommand();
75
+ });
76
+
77
+ auth
78
+ .command('status')
79
+ .description('Show authentication status')
80
+ .action(async () => {
81
+ await authStatusCommand();
82
+ });
83
+
84
+ // Sync command
85
+ program
86
+ .command('sync')
87
+ .description('Sync balance with API')
88
+ .action(async () => {
89
+ await syncCommand();
90
+ });
91
+
92
+ // Config commands
93
+ const config = program
94
+ .command('config')
95
+ .description('Manage CLI configuration');
96
+
97
+ config
98
+ .command('get')
99
+ .description('Get configuration value')
100
+ .argument('<key>', 'Configuration key (api-url, sync-enabled, all)')
101
+ .action(async (key: string) => {
102
+ await configGetCommand(key);
103
+ });
104
+
105
+ config
106
+ .command('set')
107
+ .description('Set configuration value')
108
+ .argument('<key>', 'Configuration key (api-url, sync-enabled)')
109
+ .argument('<value>', 'Configuration value')
110
+ .action(async (key: string, value: string) => {
111
+ await configSetCommand(key, value);
112
+ });
113
+
114
+ program.parse();
@@ -0,0 +1,44 @@
1
+ import { detectPattern, PatternType } from './patterns';
2
+
3
+ describe('Pattern Detection', () => {
4
+ it('detects all same character', () => {
5
+ const result = detectPattern('aaaaaaa');
6
+ expect(result.type).toBe(PatternType.ALL_SAME);
7
+ expect(result.name).toBe('JACKPOT');
8
+ });
9
+
10
+ it('detects 4 of a kind', () => {
11
+ const result = detectPattern('aaaa123');
12
+ expect(result.type).toBe(PatternType.FOUR_OF_KIND);
13
+ expect(result.payout).toBeGreaterThan(0);
14
+ });
15
+
16
+ it('detects fullest house (4-3)', () => {
17
+ const result = detectPattern('aaaabbb');
18
+ expect(result.type).toBe(PatternType.FULLEST_HOUSE);
19
+ });
20
+
21
+ it('detects three pair', () => {
22
+ const result = detectPattern('aabbcc1');
23
+ expect(result.type).toBe(PatternType.THREE_PAIR);
24
+ });
25
+
26
+ it('detects one pair', () => {
27
+ const result = detectPattern('aa12345');
28
+ expect(result.type).toBe(PatternType.ONE_PAIR);
29
+ });
30
+
31
+ it('detects no win', () => {
32
+ const result = detectPattern('1234567');
33
+ expect(result.type).toBe(PatternType.NO_WIN);
34
+ expect(result.payout).toBe(0);
35
+ });
36
+
37
+ it('validates hash length', () => {
38
+ expect(() => detectPattern('abc')).toThrow('Hash must be 7 characters');
39
+ });
40
+
41
+ it('validates hex characters', () => {
42
+ expect(() => detectPattern('gggggg1')).toThrow('Hash must contain only hex characters');
43
+ });
44
+ });