@snapcommit/cli 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/README.md +162 -0
- package/dist/ai/anthropic-client.js +92 -0
- package/dist/ai/commit-generator.js +200 -0
- package/dist/ai/gemini-client.js +201 -0
- package/dist/ai/git-interpreter.js +209 -0
- package/dist/ai/smart-solver.js +260 -0
- package/dist/auth/supabase-client.js +288 -0
- package/dist/commands/activate.js +108 -0
- package/dist/commands/commit.js +255 -0
- package/dist/commands/conflict.js +233 -0
- package/dist/commands/doctor.js +113 -0
- package/dist/commands/git-advanced.js +311 -0
- package/dist/commands/github-auth.js +193 -0
- package/dist/commands/login.js +11 -0
- package/dist/commands/natural.js +305 -0
- package/dist/commands/onboard.js +111 -0
- package/dist/commands/quick.js +173 -0
- package/dist/commands/setup.js +163 -0
- package/dist/commands/stats.js +128 -0
- package/dist/commands/uninstall.js +131 -0
- package/dist/db/database.js +99 -0
- package/dist/index.js +144 -0
- package/dist/lib/auth.js +171 -0
- package/dist/lib/github.js +280 -0
- package/dist/lib/multi-repo.js +276 -0
- package/dist/lib/supabase.js +153 -0
- package/dist/license/manager.js +203 -0
- package/dist/repl/index.js +185 -0
- package/dist/repl/interpreter.js +524 -0
- package/dist/utils/analytics.js +36 -0
- package/dist/utils/auth-storage.js +65 -0
- package/dist/utils/dopamine.js +211 -0
- package/dist/utils/errors.js +56 -0
- package/dist/utils/git.js +105 -0
- package/dist/utils/heatmap.js +265 -0
- package/dist/utils/rate-limit.js +68 -0
- package/dist/utils/retry.js +46 -0
- package/dist/utils/ui.js +189 -0
- package/dist/utils/version.js +81 -0
- package/package.json +69 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logActivity = logActivity;
|
|
7
|
+
exports.logCommit = logCommit;
|
|
8
|
+
exports.getStats = getStats;
|
|
9
|
+
exports.logAnalyticsEvent = logAnalyticsEvent;
|
|
10
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const DB_DIR = path_1.default.join(os_1.default.homedir(), '.snapcommit');
|
|
15
|
+
const DB_PATH = path_1.default.join(DB_DIR, 'snapcommit.db');
|
|
16
|
+
// Ensure directory exists
|
|
17
|
+
if (!fs_1.default.existsSync(DB_DIR)) {
|
|
18
|
+
fs_1.default.mkdirSync(DB_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const db = new better_sqlite3_1.default(DB_PATH);
|
|
21
|
+
// Initialize database schema
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS activities (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
type TEXT NOT NULL,
|
|
26
|
+
command TEXT,
|
|
27
|
+
description TEXT,
|
|
28
|
+
timestamp INTEGER NOT NULL,
|
|
29
|
+
duration INTEGER,
|
|
30
|
+
metadata TEXT
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_activities_timestamp ON activities(timestamp);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_activities_type ON activities(type);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS commits (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
message TEXT NOT NULL,
|
|
39
|
+
hash TEXT,
|
|
40
|
+
files_changed INTEGER,
|
|
41
|
+
insertions INTEGER,
|
|
42
|
+
deletions INTEGER,
|
|
43
|
+
timestamp INTEGER NOT NULL
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_commits_timestamp ON commits(timestamp);
|
|
47
|
+
`);
|
|
48
|
+
function logActivity(activity) {
|
|
49
|
+
const stmt = db.prepare(`
|
|
50
|
+
INSERT INTO activities (type, command, description, timestamp, duration, metadata)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
52
|
+
`);
|
|
53
|
+
stmt.run(activity.type, activity.command || null, activity.description || null, activity.timestamp, activity.duration || null, activity.metadata ? JSON.stringify(activity.metadata) : null);
|
|
54
|
+
}
|
|
55
|
+
function logCommit(commit) {
|
|
56
|
+
const stmt = db.prepare(`
|
|
57
|
+
INSERT INTO commits (message, hash, files_changed, insertions, deletions, timestamp)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
59
|
+
`);
|
|
60
|
+
stmt.run(commit.message, commit.hash || null, commit.files_changed || null, commit.insertions || null, commit.deletions || null, commit.timestamp);
|
|
61
|
+
}
|
|
62
|
+
function getStats(daysBack = 7) {
|
|
63
|
+
const since = Date.now() - daysBack * 24 * 60 * 60 * 1000;
|
|
64
|
+
const totalCommits = db
|
|
65
|
+
.prepare('SELECT COUNT(*) as count FROM commits WHERE timestamp > ?')
|
|
66
|
+
.get(since);
|
|
67
|
+
const totalCommands = db
|
|
68
|
+
.prepare('SELECT COUNT(*) as count FROM activities WHERE type = "command" AND timestamp > ?')
|
|
69
|
+
.get(since);
|
|
70
|
+
const recentCommits = db
|
|
71
|
+
.prepare('SELECT * FROM commits WHERE timestamp > ? ORDER BY timestamp DESC LIMIT 10')
|
|
72
|
+
.all(since);
|
|
73
|
+
const totalInsertions = db
|
|
74
|
+
.prepare('SELECT SUM(insertions) as sum FROM commits WHERE timestamp > ?')
|
|
75
|
+
.get(since);
|
|
76
|
+
const totalDeletions = db
|
|
77
|
+
.prepare('SELECT SUM(deletions) as sum FROM commits WHERE timestamp > ?')
|
|
78
|
+
.get(since);
|
|
79
|
+
return {
|
|
80
|
+
totalCommits: totalCommits.count,
|
|
81
|
+
totalCommands: totalCommands.count,
|
|
82
|
+
totalInsertions: totalInsertions.sum || 0,
|
|
83
|
+
totalDeletions: totalDeletions.sum || 0,
|
|
84
|
+
recentCommits,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Analytics
|
|
88
|
+
function logAnalyticsEvent(event, data = {}) {
|
|
89
|
+
try {
|
|
90
|
+
db.prepare(`
|
|
91
|
+
INSERT INTO activities (type, command, description, timestamp, metadata)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?)
|
|
93
|
+
`).run('analytics', event, JSON.stringify(data), Date.now(), null);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// Silent fail - don't break user experience
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.default = db;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const dotenv_1 = require("dotenv");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const version_1 = require("./utils/version");
|
|
12
|
+
const commit_1 = require("./commands/commit");
|
|
13
|
+
const stats_1 = require("./commands/stats");
|
|
14
|
+
const setup_1 = require("./commands/setup");
|
|
15
|
+
const quick_1 = require("./commands/quick");
|
|
16
|
+
const doctor_1 = require("./commands/doctor");
|
|
17
|
+
const activate_1 = require("./commands/activate");
|
|
18
|
+
const onboard_1 = require("./commands/onboard");
|
|
19
|
+
const login_1 = require("./commands/login");
|
|
20
|
+
const natural_1 = require("./commands/natural");
|
|
21
|
+
const conflict_1 = require("./commands/conflict");
|
|
22
|
+
const uninstall_1 = require("./commands/uninstall");
|
|
23
|
+
const repl_1 = require("./repl");
|
|
24
|
+
// Load environment variables from root .env
|
|
25
|
+
(0, dotenv_1.config)({ path: path_1.default.join(__dirname, '../../.env') });
|
|
26
|
+
const program = new commander_1.Command();
|
|
27
|
+
// Check for updates (async, non-blocking)
|
|
28
|
+
(0, version_1.checkForUpdates)().then((result) => {
|
|
29
|
+
if (result && result.hasUpdate) {
|
|
30
|
+
console.log(chalk_1.default.yellow(`\n⚠️ Update available: ${result.currentVersion} → ${result.latestVersion}`));
|
|
31
|
+
console.log(chalk_1.default.gray(' Run: npm install -g builderos\n'));
|
|
32
|
+
}
|
|
33
|
+
}).catch(() => {
|
|
34
|
+
// Silent fail
|
|
35
|
+
});
|
|
36
|
+
program
|
|
37
|
+
.name('snapcommit')
|
|
38
|
+
.description('Snap. Commit. Track. - Instant AI commits with beautiful stats')
|
|
39
|
+
.version('1.0.0');
|
|
40
|
+
// Command: devflow commit
|
|
41
|
+
program
|
|
42
|
+
.command('commit')
|
|
43
|
+
.alias('c')
|
|
44
|
+
.description('Generate AI-powered commit message (interactive)')
|
|
45
|
+
.action(async () => {
|
|
46
|
+
await (0, commit_1.commitCommand)();
|
|
47
|
+
});
|
|
48
|
+
// Command: devflow quick
|
|
49
|
+
program
|
|
50
|
+
.command('quick')
|
|
51
|
+
.alias('q')
|
|
52
|
+
.description('Quick commit (stage all + AI commit, no prompts)')
|
|
53
|
+
.action(async () => {
|
|
54
|
+
await (0, quick_1.quickCommand)();
|
|
55
|
+
});
|
|
56
|
+
// Command: devflow stats
|
|
57
|
+
program
|
|
58
|
+
.command('stats')
|
|
59
|
+
.alias('s')
|
|
60
|
+
.description('Show your coding stats')
|
|
61
|
+
.action(() => {
|
|
62
|
+
(0, stats_1.statsCommand)();
|
|
63
|
+
});
|
|
64
|
+
// Command: devflow setup
|
|
65
|
+
program
|
|
66
|
+
.command('setup')
|
|
67
|
+
.description('Set up DevFlow shell integration')
|
|
68
|
+
.action(() => {
|
|
69
|
+
(0, setup_1.setupCommand)();
|
|
70
|
+
});
|
|
71
|
+
// Command: snapcommit doctor
|
|
72
|
+
program
|
|
73
|
+
.command('doctor')
|
|
74
|
+
.alias('check')
|
|
75
|
+
.description('Check if SnapCommit is set up correctly')
|
|
76
|
+
.action(() => {
|
|
77
|
+
(0, doctor_1.doctorCommand)();
|
|
78
|
+
});
|
|
79
|
+
// Command: snapcommit activate
|
|
80
|
+
program
|
|
81
|
+
.command('activate [license-key]')
|
|
82
|
+
.description('Activate your Pro license')
|
|
83
|
+
.action(async (licenseKey) => {
|
|
84
|
+
await (0, activate_1.activateCommand)(licenseKey);
|
|
85
|
+
});
|
|
86
|
+
// Command: snapcommit status
|
|
87
|
+
program
|
|
88
|
+
.command('status')
|
|
89
|
+
.description('Check your license status')
|
|
90
|
+
.action(() => {
|
|
91
|
+
(0, activate_1.statusCommand)();
|
|
92
|
+
});
|
|
93
|
+
// Command: snapcommit onboard
|
|
94
|
+
program
|
|
95
|
+
.command('onboard')
|
|
96
|
+
.alias('welcome')
|
|
97
|
+
.description('Interactive onboarding tour')
|
|
98
|
+
.action(async () => {
|
|
99
|
+
await (0, onboard_1.onboardCommand)();
|
|
100
|
+
});
|
|
101
|
+
// Command: snapcommit login
|
|
102
|
+
program
|
|
103
|
+
.command('login')
|
|
104
|
+
.description('Sign in or sign up to SnapCommit')
|
|
105
|
+
.action(async () => {
|
|
106
|
+
await (0, login_1.loginCommand)();
|
|
107
|
+
});
|
|
108
|
+
// Command: snapcommit logout
|
|
109
|
+
program
|
|
110
|
+
.command('logout')
|
|
111
|
+
.description('Sign out from SnapCommit')
|
|
112
|
+
.action(async () => {
|
|
113
|
+
await (0, login_1.logoutCommand)();
|
|
114
|
+
});
|
|
115
|
+
// Command: snapcommit conflict
|
|
116
|
+
program
|
|
117
|
+
.command('conflict')
|
|
118
|
+
.alias('resolve')
|
|
119
|
+
.description('AI-powered conflict resolution wizard')
|
|
120
|
+
.action(async () => {
|
|
121
|
+
await (0, conflict_1.conflictCommand)();
|
|
122
|
+
});
|
|
123
|
+
// Command: snapcommit uninstall
|
|
124
|
+
program
|
|
125
|
+
.command('uninstall')
|
|
126
|
+
.description('Remove SnapCommit shell integration')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
await (0, uninstall_1.uninstallCommand)();
|
|
129
|
+
});
|
|
130
|
+
// Command: snapcommit <natural language>
|
|
131
|
+
// This catches any non-matching command and treats it as natural language
|
|
132
|
+
program
|
|
133
|
+
.command('* <words...>')
|
|
134
|
+
.description('Natural language Git commands (e.g., "undo that", "merge feature")')
|
|
135
|
+
.action(async (words) => {
|
|
136
|
+
const userInput = words.join(' ');
|
|
137
|
+
await (0, natural_1.naturalCommand)(userInput);
|
|
138
|
+
});
|
|
139
|
+
// Default action - Start REPL if no command is given
|
|
140
|
+
program.action(async () => {
|
|
141
|
+
// If user just types "snapcommit" or "snap", enter REPL mode
|
|
142
|
+
await (0, repl_1.startREPL)();
|
|
143
|
+
});
|
|
144
|
+
program.parse();
|
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isAuthenticated = isAuthenticated;
|
|
7
|
+
exports.getAuthConfig = getAuthConfig;
|
|
8
|
+
exports.clearAuth = clearAuth;
|
|
9
|
+
exports.promptAuth = promptAuth;
|
|
10
|
+
exports.ensureAuth = ensureAuth;
|
|
11
|
+
exports.getToken = getToken;
|
|
12
|
+
exports.logout = logout;
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const os_1 = __importDefault(require("os"));
|
|
16
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
17
|
+
const readline_1 = __importDefault(require("readline"));
|
|
18
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.snapcommit');
|
|
19
|
+
const AUTH_FILE = path_1.default.join(CONFIG_DIR, 'auth.json');
|
|
20
|
+
// API URL - defaults to production, can be overridden for development
|
|
21
|
+
const API_BASE_URL = process.env.SNAPCOMMIT_API_URL || 'https://snapcommit.dev';
|
|
22
|
+
/**
|
|
23
|
+
* Ensure config directory exists
|
|
24
|
+
*/
|
|
25
|
+
function ensureConfigDir() {
|
|
26
|
+
if (!fs_1.default.existsSync(CONFIG_DIR)) {
|
|
27
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if user is authenticated
|
|
32
|
+
*/
|
|
33
|
+
function isAuthenticated() {
|
|
34
|
+
return fs_1.default.existsSync(AUTH_FILE);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get stored authentication config
|
|
38
|
+
*/
|
|
39
|
+
function getAuthConfig() {
|
|
40
|
+
if (!fs_1.default.existsSync(AUTH_FILE)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const data = fs_1.default.readFileSync(AUTH_FILE, 'utf-8');
|
|
45
|
+
return JSON.parse(data);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Save authentication config
|
|
53
|
+
*/
|
|
54
|
+
function saveAuthConfig(config) {
|
|
55
|
+
ensureConfigDir();
|
|
56
|
+
fs_1.default.writeFileSync(AUTH_FILE, JSON.stringify(config, null, 2));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Clear authentication
|
|
60
|
+
*/
|
|
61
|
+
function clearAuth() {
|
|
62
|
+
if (fs_1.default.existsSync(AUTH_FILE)) {
|
|
63
|
+
fs_1.default.unlinkSync(AUTH_FILE);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ask a question and return the answer
|
|
68
|
+
*/
|
|
69
|
+
function askQuestion(query) {
|
|
70
|
+
const rl = readline_1.default.createInterface({
|
|
71
|
+
input: process.stdin,
|
|
72
|
+
output: process.stdout,
|
|
73
|
+
});
|
|
74
|
+
return new Promise((resolve) => rl.question(query, (ans) => {
|
|
75
|
+
rl.close();
|
|
76
|
+
resolve(ans);
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Verify token with backend
|
|
81
|
+
*/
|
|
82
|
+
async function verifyToken(token) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(`${API_BASE_URL}/api/auth/token?token=${encodeURIComponent(token)}`);
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
if (response.ok && data.valid) {
|
|
87
|
+
return { valid: true, user: data.user };
|
|
88
|
+
}
|
|
89
|
+
return { valid: false };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new Error('Failed to verify token. Please check your internet connection.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Prompt user to authenticate
|
|
97
|
+
*/
|
|
98
|
+
async function promptAuth() {
|
|
99
|
+
console.log(chalk_1.default.bold.cyan('\n🔐 Welcome to SnapCommit!\n'));
|
|
100
|
+
console.log(chalk_1.default.gray('To use SnapCommit, you need to authenticate.\n'));
|
|
101
|
+
console.log(chalk_1.default.bold('Step 1:') + chalk_1.default.gray(' Sign up or log in at:'));
|
|
102
|
+
console.log(chalk_1.default.cyan.underline(' 👉 https://snapcommit.dev/login\n'));
|
|
103
|
+
console.log(chalk_1.default.bold('Step 2:') + chalk_1.default.gray(' Generate a token from your dashboard'));
|
|
104
|
+
console.log(chalk_1.default.gray(' (You\'ll see a "Generate Token" button)\n'));
|
|
105
|
+
const token = await askQuestion(chalk_1.default.yellow('Step 3: Paste your token here: '));
|
|
106
|
+
if (!token || token.trim().length === 0) {
|
|
107
|
+
console.log(chalk_1.default.red('\n❌ No token provided. Authentication cancelled.\n'));
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
console.log(chalk_1.default.gray('\n🔄 Verifying token...\n'));
|
|
111
|
+
try {
|
|
112
|
+
const result = await verifyToken(token.trim());
|
|
113
|
+
if (result.valid && result.user) {
|
|
114
|
+
const config = {
|
|
115
|
+
token: token.trim(),
|
|
116
|
+
userId: result.user.id,
|
|
117
|
+
email: result.user.email,
|
|
118
|
+
name: result.user.name || 'Developer',
|
|
119
|
+
};
|
|
120
|
+
saveAuthConfig(config);
|
|
121
|
+
console.log(chalk_1.default.green(`✅ Authenticated successfully! Welcome, ${chalk_1.default.bold(config.name)}!\n`));
|
|
122
|
+
console.log(chalk_1.default.gray('💎 SnapCommit Pro - Unlimited AI commits'));
|
|
123
|
+
console.log(chalk_1.default.gray('🔥 Your coding journey starts now!\n'));
|
|
124
|
+
console.log(chalk_1.default.gray('Try: snap quick (in any git repo)\n'));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(chalk_1.default.red('\n❌ Invalid token. Please try again.\n'));
|
|
129
|
+
console.log(chalk_1.default.gray('Make sure you copied the entire token from your dashboard.\n'));
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.log(chalk_1.default.red(`\n❌ ${error.message}\n`));
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Ensure user is authenticated (prompt if not)
|
|
140
|
+
*/
|
|
141
|
+
async function ensureAuth() {
|
|
142
|
+
if (isAuthenticated()) {
|
|
143
|
+
return getAuthConfig();
|
|
144
|
+
}
|
|
145
|
+
const success = await promptAuth();
|
|
146
|
+
if (success) {
|
|
147
|
+
return getAuthConfig();
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get authentication token for API calls
|
|
153
|
+
*/
|
|
154
|
+
function getToken() {
|
|
155
|
+
const config = getAuthConfig();
|
|
156
|
+
return config?.token || null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Logout command
|
|
160
|
+
*/
|
|
161
|
+
async function logout() {
|
|
162
|
+
if (!isAuthenticated()) {
|
|
163
|
+
console.log(chalk_1.default.gray('\n Not currently logged in.\n'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const config = getAuthConfig();
|
|
167
|
+
console.log(chalk_1.default.yellow(`\n Logging out ${config?.email}...\n`));
|
|
168
|
+
clearAuth();
|
|
169
|
+
console.log(chalk_1.default.green(' ✅ Logged out successfully!\n'));
|
|
170
|
+
console.log(chalk_1.default.gray(' Run "snapcommit" again to log back in.\n'));
|
|
171
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getCurrentRepo = getCurrentRepo;
|
|
7
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
8
|
+
exports.createPullRequest = createPullRequest;
|
|
9
|
+
exports.listPullRequests = listPullRequests;
|
|
10
|
+
exports.getPullRequest = getPullRequest;
|
|
11
|
+
exports.mergePullRequest = mergePullRequest;
|
|
12
|
+
exports.getCommitStatus = getCommitStatus;
|
|
13
|
+
exports.listWorkflowRuns = listWorkflowRuns;
|
|
14
|
+
exports.triggerWorkflow = triggerWorkflow;
|
|
15
|
+
exports.createIssue = createIssue;
|
|
16
|
+
exports.listIssues = listIssues;
|
|
17
|
+
exports.closeIssue = closeIssue;
|
|
18
|
+
exports.createRelease = createRelease;
|
|
19
|
+
exports.getRepoInfo = getRepoInfo;
|
|
20
|
+
const child_process_1 = require("child_process");
|
|
21
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
22
|
+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
|
23
|
+
const GITHUB_API = 'https://api.github.com';
|
|
24
|
+
if (!GITHUB_TOKEN) {
|
|
25
|
+
console.warn(chalk_1.default.yellow('⚠️ GitHub token not found. GitHub features will be disabled.'));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get current repository info from git remote
|
|
29
|
+
*/
|
|
30
|
+
function getCurrentRepo() {
|
|
31
|
+
try {
|
|
32
|
+
const remote = (0, child_process_1.execSync)('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
33
|
+
// Parse GitHub URL (supports both HTTPS and SSH)
|
|
34
|
+
let match;
|
|
35
|
+
if (remote.startsWith('https://')) {
|
|
36
|
+
// https://github.com/owner/repo.git
|
|
37
|
+
match = remote.match(/github\.com[/:]([\w-]+)\/([\w-]+?)(\.git)?$/);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
// git@github.com:owner/repo.git
|
|
41
|
+
match = remote.match(/github\.com[/:]([\w-]+)\/([\w-]+?)(\.git)?$/);
|
|
42
|
+
}
|
|
43
|
+
if (match) {
|
|
44
|
+
return {
|
|
45
|
+
owner: match[1],
|
|
46
|
+
name: match[2],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get current branch name
|
|
57
|
+
*/
|
|
58
|
+
function getCurrentBranch() {
|
|
59
|
+
try {
|
|
60
|
+
return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return 'main';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* GitHub API request helper
|
|
68
|
+
*/
|
|
69
|
+
async function githubRequest(endpoint, options = {}) {
|
|
70
|
+
if (!GITHUB_TOKEN) {
|
|
71
|
+
throw new Error('GitHub token not configured. Please set GITHUB_TOKEN in your .env file.');
|
|
72
|
+
}
|
|
73
|
+
const url = `${GITHUB_API}${endpoint}`;
|
|
74
|
+
const headers = {
|
|
75
|
+
Authorization: `Bearer ${GITHUB_TOKEN}`,
|
|
76
|
+
Accept: 'application/vnd.github+json',
|
|
77
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
...options.headers,
|
|
80
|
+
};
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
...options,
|
|
83
|
+
headers,
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
87
|
+
throw new Error(`GitHub API error: ${error.message || response.statusText}`);
|
|
88
|
+
}
|
|
89
|
+
return response.json();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create a Pull Request
|
|
93
|
+
*/
|
|
94
|
+
async function createPullRequest(options) {
|
|
95
|
+
const repo = getCurrentRepo();
|
|
96
|
+
if (!repo) {
|
|
97
|
+
throw new Error('Not a GitHub repository');
|
|
98
|
+
}
|
|
99
|
+
const currentBranch = options.head || getCurrentBranch();
|
|
100
|
+
const baseBranch = options.base || 'main';
|
|
101
|
+
// Get last commit message for default title
|
|
102
|
+
let defaultTitle = 'Update from ' + currentBranch;
|
|
103
|
+
try {
|
|
104
|
+
defaultTitle = (0, child_process_1.execSync)('git log -1 --pretty=%s', { encoding: 'utf-8' }).trim();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Use default
|
|
108
|
+
}
|
|
109
|
+
const title = options.title || defaultTitle;
|
|
110
|
+
const body = options.body || '';
|
|
111
|
+
const pr = await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
title,
|
|
115
|
+
body,
|
|
116
|
+
head: currentBranch,
|
|
117
|
+
base: baseBranch,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
return pr;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* List Pull Requests
|
|
124
|
+
*/
|
|
125
|
+
async function listPullRequests(options) {
|
|
126
|
+
const repo = getCurrentRepo();
|
|
127
|
+
if (!repo) {
|
|
128
|
+
throw new Error('Not a GitHub repository');
|
|
129
|
+
}
|
|
130
|
+
const state = options?.state || 'open';
|
|
131
|
+
const limit = options?.limit || 10;
|
|
132
|
+
const prs = await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls?state=${state}&per_page=${limit}`);
|
|
133
|
+
return prs;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get PR by number
|
|
137
|
+
*/
|
|
138
|
+
async function getPullRequest(prNumber) {
|
|
139
|
+
const repo = getCurrentRepo();
|
|
140
|
+
if (!repo) {
|
|
141
|
+
throw new Error('Not a GitHub repository');
|
|
142
|
+
}
|
|
143
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Merge a Pull Request
|
|
147
|
+
*/
|
|
148
|
+
async function mergePullRequest(prNumber, options) {
|
|
149
|
+
const repo = getCurrentRepo();
|
|
150
|
+
if (!repo) {
|
|
151
|
+
throw new Error('Not a GitHub repository');
|
|
152
|
+
}
|
|
153
|
+
const mergeMethod = options?.mergeMethod || 'merge';
|
|
154
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls/${prNumber}/merge`, {
|
|
155
|
+
method: 'PUT',
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
merge_method: mergeMethod,
|
|
158
|
+
commit_title: options?.commitTitle,
|
|
159
|
+
commit_message: options?.commitMessage,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check CI/CD status for a commit or PR
|
|
165
|
+
*/
|
|
166
|
+
async function getCommitStatus(commitSha) {
|
|
167
|
+
const repo = getCurrentRepo();
|
|
168
|
+
if (!repo) {
|
|
169
|
+
throw new Error('Not a GitHub repository');
|
|
170
|
+
}
|
|
171
|
+
// Get current commit SHA if not provided
|
|
172
|
+
const sha = commitSha || (0, child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
173
|
+
const status = await githubRequest(`/repos/${repo.owner}/${repo.name}/commits/${sha}/status`);
|
|
174
|
+
const checks = await githubRequest(`/repos/${repo.owner}/${repo.name}/commits/${sha}/check-runs`);
|
|
175
|
+
return {
|
|
176
|
+
status,
|
|
177
|
+
checks: checks.check_runs || [],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* List workflow runs
|
|
182
|
+
*/
|
|
183
|
+
async function listWorkflowRuns(options) {
|
|
184
|
+
const repo = getCurrentRepo();
|
|
185
|
+
if (!repo) {
|
|
186
|
+
throw new Error('Not a GitHub repository');
|
|
187
|
+
}
|
|
188
|
+
const limit = options?.limit || 10;
|
|
189
|
+
const endpoint = options?.workflowId
|
|
190
|
+
? `/repos/${repo.owner}/${repo.name}/actions/workflows/${options.workflowId}/runs?per_page=${limit}`
|
|
191
|
+
: `/repos/${repo.owner}/${repo.name}/actions/runs?per_page=${limit}`;
|
|
192
|
+
return await githubRequest(endpoint);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Trigger a workflow
|
|
196
|
+
*/
|
|
197
|
+
async function triggerWorkflow(workflowId, ref = 'main', inputs) {
|
|
198
|
+
const repo = getCurrentRepo();
|
|
199
|
+
if (!repo) {
|
|
200
|
+
throw new Error('Not a GitHub repository');
|
|
201
|
+
}
|
|
202
|
+
await githubRequest(`/repos/${repo.owner}/${repo.name}/actions/workflows/${workflowId}/dispatches`, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
ref,
|
|
206
|
+
inputs,
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Create an issue
|
|
212
|
+
*/
|
|
213
|
+
async function createIssue(options) {
|
|
214
|
+
const repo = getCurrentRepo();
|
|
215
|
+
if (!repo) {
|
|
216
|
+
throw new Error('Not a GitHub repository');
|
|
217
|
+
}
|
|
218
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/issues`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: JSON.stringify(options),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* List issues
|
|
225
|
+
*/
|
|
226
|
+
async function listIssues(options) {
|
|
227
|
+
const repo = getCurrentRepo();
|
|
228
|
+
if (!repo) {
|
|
229
|
+
throw new Error('Not a GitHub repository');
|
|
230
|
+
}
|
|
231
|
+
const state = options?.state || 'open';
|
|
232
|
+
const limit = options?.limit || 10;
|
|
233
|
+
let endpoint = `/repos/${repo.owner}/${repo.name}/issues?state=${state}&per_page=${limit}`;
|
|
234
|
+
if (options?.labels && options.labels.length > 0) {
|
|
235
|
+
endpoint += `&labels=${options.labels.join(',')}`;
|
|
236
|
+
}
|
|
237
|
+
return await githubRequest(endpoint);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Close an issue
|
|
241
|
+
*/
|
|
242
|
+
async function closeIssue(issueNumber) {
|
|
243
|
+
const repo = getCurrentRepo();
|
|
244
|
+
if (!repo) {
|
|
245
|
+
throw new Error('Not a GitHub repository');
|
|
246
|
+
}
|
|
247
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/issues/${issueNumber}`, {
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
body: JSON.stringify({ state: 'closed' }),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Create a release
|
|
254
|
+
*/
|
|
255
|
+
async function createRelease(options) {
|
|
256
|
+
const repo = getCurrentRepo();
|
|
257
|
+
if (!repo) {
|
|
258
|
+
throw new Error('Not a GitHub repository');
|
|
259
|
+
}
|
|
260
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}/releases`, {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
body: JSON.stringify({
|
|
263
|
+
tag_name: options.tagName,
|
|
264
|
+
name: options.name || options.tagName,
|
|
265
|
+
body: options.body || '',
|
|
266
|
+
draft: options.draft || false,
|
|
267
|
+
prerelease: options.prerelease || false,
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get repository info
|
|
273
|
+
*/
|
|
274
|
+
async function getRepoInfo() {
|
|
275
|
+
const repo = getCurrentRepo();
|
|
276
|
+
if (!repo) {
|
|
277
|
+
throw new Error('Not a GitHub repository');
|
|
278
|
+
}
|
|
279
|
+
return await githubRequest(`/repos/${repo.owner}/${repo.name}`);
|
|
280
|
+
}
|