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.
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/dist/animation/slotMachine.d.ts +9 -0
- package/dist/animation/slotMachine.d.ts.map +1 -0
- package/dist/animation/slotMachine.js +139 -0
- package/dist/animation/slotMachine.js.map +1 -0
- package/dist/api.d.ts +40 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +154 -0
- package/dist/api.js.map +1 -0
- package/dist/balance.d.ts +11 -0
- package/dist/balance.d.ts.map +1 -0
- package/dist/balance.js +131 -0
- package/dist/balance.js.map +1 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +91 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.d.ts.map +1 -0
- package/dist/commands/balance.js +32 -0
- package/dist/commands/balance.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +60 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +148 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/play.d.ts +7 -0
- package/dist/commands/play.d.ts.map +1 -0
- package/dist/commands/play.js +137 -0
- package/dist/commands/play.js.map +1 -0
- package/dist/commands/spin.d.ts +6 -0
- package/dist/commands/spin.d.ts.map +1 -0
- package/dist/commands/spin.js +17 -0
- package/dist/commands/spin.js.map +1 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +52 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/test.d.ts +6 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +17 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +144 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +100 -0
- package/dist/index.js.map +1 -0
- package/dist/patterns.d.ts +27 -0
- package/dist/patterns.d.ts.map +1 -0
- package/dist/patterns.js +266 -0
- package/dist/patterns.js.map +1 -0
- package/dist/templates/post-commit.d.ts +2 -0
- package/dist/templates/post-commit.d.ts.map +1 -0
- package/dist/templates/post-commit.js +19 -0
- package/dist/templates/post-commit.js.map +1 -0
- package/dist/utils/git.d.ts +4 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/git.js.map +1 -0
- package/jest.config.js +12 -0
- package/package.json +50 -0
- package/src/animation/slotMachine.ts +159 -0
- package/src/api.ts +203 -0
- package/src/balance.ts +118 -0
- package/src/commands/auth.ts +92 -0
- package/src/commands/balance.ts +28 -0
- package/src/commands/config.ts +59 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/play.ts +150 -0
- package/src/commands/spin.ts +17 -0
- package/src/commands/sync.ts +49 -0
- package/src/commands/test.ts +19 -0
- package/src/config.ts +154 -0
- package/src/index.ts +114 -0
- package/src/patterns.test.ts +44 -0
- package/src/patterns.ts +292 -0
- package/src/templates/post-commit.ts +15 -0
- package/src/utils/git.ts +38 -0
- package/test.txt +2 -0
- 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
|
+
});
|