@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,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.loadRepos = loadRepos;
|
|
40
|
+
exports.getCurrentRepoPath = getCurrentRepoPath;
|
|
41
|
+
exports.getRepoName = getRepoName;
|
|
42
|
+
exports.saveCurrentRepo = saveCurrentRepo;
|
|
43
|
+
exports.listRepos = listRepos;
|
|
44
|
+
exports.switchToRepo = switchToRepo;
|
|
45
|
+
exports.getRepoContext = getRepoContext;
|
|
46
|
+
const child_process_1 = require("child_process");
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
51
|
+
const REPOS_CONFIG_PATH = path.join(os.homedir(), '.snapcommit', 'repos.json');
|
|
52
|
+
/**
|
|
53
|
+
* Load saved repositories
|
|
54
|
+
*/
|
|
55
|
+
function loadRepos() {
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(REPOS_CONFIG_PATH)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const data = fs.readFileSync(REPOS_CONFIG_PATH, 'utf-8');
|
|
61
|
+
return JSON.parse(data);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Save repositories
|
|
69
|
+
*/
|
|
70
|
+
function saveRepos(repos) {
|
|
71
|
+
try {
|
|
72
|
+
const dir = path.dirname(REPOS_CONFIG_PATH);
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(REPOS_CONFIG_PATH, JSON.stringify(repos, null, 2));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(chalk_1.default.red('Failed to save repo config'));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get current repository path
|
|
84
|
+
*/
|
|
85
|
+
function getCurrentRepoPath() {
|
|
86
|
+
try {
|
|
87
|
+
const repoPath = (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
|
|
88
|
+
encoding: 'utf-8',
|
|
89
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
90
|
+
}).trim();
|
|
91
|
+
return repoPath;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get repository name from path
|
|
99
|
+
*/
|
|
100
|
+
function getRepoName(repoPath) {
|
|
101
|
+
return path.basename(repoPath);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Add current repository to saved repos
|
|
105
|
+
*/
|
|
106
|
+
function saveCurrentRepo() {
|
|
107
|
+
const repoPath = getCurrentRepoPath();
|
|
108
|
+
if (!repoPath) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const repos = loadRepos();
|
|
112
|
+
const name = getRepoName(repoPath);
|
|
113
|
+
// Get current branch
|
|
114
|
+
let branch;
|
|
115
|
+
try {
|
|
116
|
+
branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
|
|
117
|
+
encoding: 'utf-8',
|
|
118
|
+
cwd: repoPath,
|
|
119
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
120
|
+
}).trim();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
branch = 'unknown';
|
|
124
|
+
}
|
|
125
|
+
// Update or add repo
|
|
126
|
+
const existingIndex = repos.findIndex(r => r.path === repoPath);
|
|
127
|
+
if (existingIndex >= 0) {
|
|
128
|
+
repos[existingIndex].lastUsed = Date.now();
|
|
129
|
+
repos[existingIndex].branch = branch;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
repos.push({
|
|
133
|
+
name,
|
|
134
|
+
path: repoPath,
|
|
135
|
+
lastUsed: Date.now(),
|
|
136
|
+
branch,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
saveRepos(repos);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* List all saved repositories
|
|
143
|
+
*/
|
|
144
|
+
function listRepos() {
|
|
145
|
+
const repos = loadRepos();
|
|
146
|
+
if (repos.length === 0) {
|
|
147
|
+
console.log(chalk_1.default.gray('\n No saved repositories\n'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Sort by last used
|
|
151
|
+
repos.sort((a, b) => b.lastUsed - a.lastUsed);
|
|
152
|
+
console.log(chalk_1.default.bold('\n📁 Saved Repositories:\n'));
|
|
153
|
+
const currentPath = getCurrentRepoPath();
|
|
154
|
+
repos.forEach((repo, index) => {
|
|
155
|
+
const isCurrent = repo.path === currentPath;
|
|
156
|
+
const icon = isCurrent ? chalk_1.default.green('→') : ' ';
|
|
157
|
+
const nameColor = isCurrent ? chalk_1.default.green.bold : chalk_1.default.cyan;
|
|
158
|
+
console.log(` ${icon} ${nameColor(repo.name)}`);
|
|
159
|
+
console.log(chalk_1.default.gray(` ${repo.path}`));
|
|
160
|
+
if (repo.branch) {
|
|
161
|
+
console.log(chalk_1.default.gray(` Branch: ${repo.branch}`));
|
|
162
|
+
}
|
|
163
|
+
console.log('');
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Switch to a repository by name or index
|
|
168
|
+
*/
|
|
169
|
+
async function switchToRepo(nameOrIndex) {
|
|
170
|
+
const repos = loadRepos();
|
|
171
|
+
if (repos.length === 0) {
|
|
172
|
+
console.log(chalk_1.default.red('\n❌ No saved repositories\n'));
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
// Sort by last used
|
|
176
|
+
repos.sort((a, b) => b.lastUsed - a.lastUsed);
|
|
177
|
+
let targetRepo;
|
|
178
|
+
if (typeof nameOrIndex === 'number') {
|
|
179
|
+
targetRepo = repos[nameOrIndex];
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
// Try to match by name (case-insensitive)
|
|
183
|
+
const search = nameOrIndex.toLowerCase();
|
|
184
|
+
targetRepo = repos.find(r => r.name.toLowerCase().includes(search) ||
|
|
185
|
+
r.path.toLowerCase().includes(search));
|
|
186
|
+
}
|
|
187
|
+
if (!targetRepo) {
|
|
188
|
+
console.log(chalk_1.default.red(`\n❌ Repository "${nameOrIndex}" not found\n`));
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
// Check if repo path still exists
|
|
192
|
+
if (!fs.existsSync(targetRepo.path)) {
|
|
193
|
+
console.log(chalk_1.default.red(`\n❌ Repository path no longer exists: ${targetRepo.path}\n`));
|
|
194
|
+
console.log(chalk_1.default.yellow(' Removing from saved repos...\n'));
|
|
195
|
+
// Remove from saved repos
|
|
196
|
+
const filtered = repos.filter(r => r.path !== targetRepo.path);
|
|
197
|
+
saveRepos(filtered);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
// Check if it's a git repo
|
|
201
|
+
try {
|
|
202
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', {
|
|
203
|
+
cwd: targetRepo.path,
|
|
204
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
console.log(chalk_1.default.red(`\n❌ Not a valid git repository: ${targetRepo.path}\n`));
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
// Change to the directory
|
|
212
|
+
try {
|
|
213
|
+
process.chdir(targetRepo.path);
|
|
214
|
+
// Update last used
|
|
215
|
+
targetRepo.lastUsed = Date.now();
|
|
216
|
+
saveRepos(repos);
|
|
217
|
+
// Get current branch
|
|
218
|
+
let branch = 'unknown';
|
|
219
|
+
try {
|
|
220
|
+
branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
|
|
221
|
+
encoding: 'utf-8',
|
|
222
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
223
|
+
}).trim();
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
// Ignore
|
|
227
|
+
}
|
|
228
|
+
// Get status
|
|
229
|
+
let statusLine = '';
|
|
230
|
+
try {
|
|
231
|
+
const status = (0, child_process_1.execSync)('git status --short', {
|
|
232
|
+
encoding: 'utf-8',
|
|
233
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
234
|
+
}).trim();
|
|
235
|
+
const changedFiles = status.split('\n').filter(line => line.trim()).length;
|
|
236
|
+
if (changedFiles > 0) {
|
|
237
|
+
statusLine = chalk_1.default.yellow(` | ${changedFiles} file${changedFiles > 1 ? 's' : ''} changed`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
statusLine = chalk_1.default.green(' | Clean');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
// Ignore
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk_1.default.green(`\n✅ Switched to ${chalk_1.default.bold(targetRepo.name)}`));
|
|
247
|
+
console.log(chalk_1.default.gray(` Branch: ${branch}${statusLine}`));
|
|
248
|
+
console.log(chalk_1.default.gray(` Path: ${targetRepo.path}\n`));
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.log(chalk_1.default.red(`\n❌ Failed to switch: ${error.message}\n`));
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Get repo context for display
|
|
258
|
+
*/
|
|
259
|
+
function getRepoContext() {
|
|
260
|
+
const repoPath = getCurrentRepoPath();
|
|
261
|
+
if (!repoPath) {
|
|
262
|
+
return chalk_1.default.gray('(not in a git repo)');
|
|
263
|
+
}
|
|
264
|
+
const name = getRepoName(repoPath);
|
|
265
|
+
let branch = 'unknown';
|
|
266
|
+
try {
|
|
267
|
+
branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
|
|
268
|
+
encoding: 'utf-8',
|
|
269
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
270
|
+
}).trim();
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
// Ignore
|
|
274
|
+
}
|
|
275
|
+
return chalk_1.default.cyan(`${name}`) + chalk_1.default.gray(` (${branch})`);
|
|
276
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.supabase = void 0;
|
|
37
|
+
exports.storeAuthTokens = storeAuthTokens;
|
|
38
|
+
exports.getStoredAuth = getStoredAuth;
|
|
39
|
+
exports.clearStoredAuth = clearStoredAuth;
|
|
40
|
+
exports.isAuthenticated = isAuthenticated;
|
|
41
|
+
exports.getCurrentUser = getCurrentUser;
|
|
42
|
+
exports.checkLicenseStatus = checkLicenseStatus;
|
|
43
|
+
exports.syncStatsToCloud = syncStatsToCloud;
|
|
44
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
49
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
50
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
51
|
+
throw new Error('Missing Supabase environment variables. Please check your .env file.');
|
|
52
|
+
}
|
|
53
|
+
exports.supabase = (0, supabase_js_1.createClient)(supabaseUrl, supabaseAnonKey);
|
|
54
|
+
// Token storage path
|
|
55
|
+
const TOKEN_DIR = path.join(os.homedir(), '.snapcommit');
|
|
56
|
+
const TOKEN_FILE = path.join(TOKEN_DIR, 'auth_token.json');
|
|
57
|
+
/**
|
|
58
|
+
* Store authentication tokens locally
|
|
59
|
+
*/
|
|
60
|
+
function storeAuthTokens(auth) {
|
|
61
|
+
if (!fs.existsSync(TOKEN_DIR)) {
|
|
62
|
+
fs.mkdirSync(TOKEN_DIR, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
fs.writeFileSync(TOKEN_FILE, JSON.stringify(auth, null, 2));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get stored authentication tokens
|
|
68
|
+
*/
|
|
69
|
+
function getStoredAuth() {
|
|
70
|
+
try {
|
|
71
|
+
if (!fs.existsSync(TOKEN_FILE)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const data = fs.readFileSync(TOKEN_FILE, 'utf-8');
|
|
75
|
+
return JSON.parse(data);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clear stored authentication tokens
|
|
83
|
+
*/
|
|
84
|
+
function clearStoredAuth() {
|
|
85
|
+
try {
|
|
86
|
+
if (fs.existsSync(TOKEN_FILE)) {
|
|
87
|
+
fs.unlinkSync(TOKEN_FILE);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// Silent fail
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if user is authenticated
|
|
96
|
+
*/
|
|
97
|
+
async function isAuthenticated() {
|
|
98
|
+
const stored = getStoredAuth();
|
|
99
|
+
if (!stored) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
// Check if token is expired
|
|
103
|
+
if (Date.now() >= stored.expires_at) {
|
|
104
|
+
clearStoredAuth();
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get current user
|
|
111
|
+
*/
|
|
112
|
+
async function getCurrentUser() {
|
|
113
|
+
const stored = getStoredAuth();
|
|
114
|
+
if (!stored) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const { data, error } = await exports.supabase.auth.getUser(stored.access_token);
|
|
118
|
+
if (error || !data.user) {
|
|
119
|
+
clearStoredAuth();
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
return data.user;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Check license status
|
|
126
|
+
*/
|
|
127
|
+
async function checkLicenseStatus(userId) {
|
|
128
|
+
const { data, error } = await exports.supabase.rpc('check_license_status', {
|
|
129
|
+
user_id_param: userId,
|
|
130
|
+
});
|
|
131
|
+
if (error) {
|
|
132
|
+
console.error('Error checking license:', error);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return data && data.length > 0 ? data[0] : null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Sync stats to cloud
|
|
139
|
+
*/
|
|
140
|
+
async function syncStatsToCloud(userId, date, commits, commands, insertions, deletions, filesChanged) {
|
|
141
|
+
const { error } = await exports.supabase.rpc('update_daily_stats', {
|
|
142
|
+
user_id_param: userId,
|
|
143
|
+
date_param: date,
|
|
144
|
+
commits_delta: commits,
|
|
145
|
+
commands_delta: commands,
|
|
146
|
+
insertions_delta: insertions,
|
|
147
|
+
deletions_delta: deletions,
|
|
148
|
+
files_changed_delta: filesChanged,
|
|
149
|
+
});
|
|
150
|
+
if (error) {
|
|
151
|
+
console.error('Error syncing stats:', error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
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.getCurrentLicense = getCurrentLicense;
|
|
7
|
+
exports.activateLicense = activateLicense;
|
|
8
|
+
exports.activateFreeTier = activateFreeTier;
|
|
9
|
+
exports.deactivateLicense = deactivateLicense;
|
|
10
|
+
exports.isProUser = isProUser;
|
|
11
|
+
exports.isTrialActive = isTrialActive;
|
|
12
|
+
exports.canUseCommit = canUseCommit;
|
|
13
|
+
exports.trackCommit = trackCommit;
|
|
14
|
+
exports.getTrialStatus = getTrialStatus;
|
|
15
|
+
exports.getLicenseInfo = getLicenseInfo;
|
|
16
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const os_1 = __importDefault(require("os"));
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const LICENSE_DIR = path_1.default.join(os_1.default.homedir(), '.builderos');
|
|
21
|
+
const LICENSE_DB = path_1.default.join(LICENSE_DIR, 'license.db');
|
|
22
|
+
// Ensure directory exists
|
|
23
|
+
if (!fs_1.default.existsSync(LICENSE_DIR)) {
|
|
24
|
+
fs_1.default.mkdirSync(LICENSE_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
const db = new better_sqlite3_1.default(LICENSE_DB);
|
|
27
|
+
// Initialize license database
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS license (
|
|
30
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
31
|
+
key TEXT UNIQUE NOT NULL,
|
|
32
|
+
email TEXT,
|
|
33
|
+
plan TEXT NOT NULL,
|
|
34
|
+
status TEXT NOT NULL,
|
|
35
|
+
activated_at INTEGER NOT NULL,
|
|
36
|
+
expires_at INTEGER,
|
|
37
|
+
device_id TEXT NOT NULL,
|
|
38
|
+
last_validated INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS usage (
|
|
42
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
month TEXT NOT NULL,
|
|
44
|
+
commits INTEGER DEFAULT 0,
|
|
45
|
+
UNIQUE(month)
|
|
46
|
+
);
|
|
47
|
+
`);
|
|
48
|
+
// Free trial: 7 days
|
|
49
|
+
const TRIAL_DAYS = 7;
|
|
50
|
+
const TRIAL_DURATION_MS = TRIAL_DAYS * 24 * 60 * 60 * 1000;
|
|
51
|
+
function getCurrentLicense() {
|
|
52
|
+
const result = db.prepare('SELECT * FROM license ORDER BY id DESC LIMIT 1').get();
|
|
53
|
+
if (!result) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
key: result.key,
|
|
58
|
+
email: result.email,
|
|
59
|
+
plan: result.plan,
|
|
60
|
+
status: result.status,
|
|
61
|
+
activated_at: result.activated_at,
|
|
62
|
+
expires_at: result.expires_at,
|
|
63
|
+
device_id: result.device_id,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function activateLicense(licenseKey, email, plan = 'pro_monthly') {
|
|
67
|
+
const deviceId = getDeviceId();
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
// Calculate expiration
|
|
70
|
+
let expires_at;
|
|
71
|
+
if (plan === 'pro_monthly') {
|
|
72
|
+
expires_at = now + 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
73
|
+
}
|
|
74
|
+
else if (plan === 'pro_yearly') {
|
|
75
|
+
expires_at = now + 365 * 24 * 60 * 60 * 1000; // 365 days
|
|
76
|
+
}
|
|
77
|
+
// Deactivate any existing license
|
|
78
|
+
db.prepare('DELETE FROM license').run();
|
|
79
|
+
// Insert new license
|
|
80
|
+
db.prepare(`
|
|
81
|
+
INSERT INTO license (key, email, plan, status, activated_at, expires_at, device_id, last_validated)
|
|
82
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
83
|
+
`).run(licenseKey, email || null, plan, 'active', now, expires_at || null, deviceId, now);
|
|
84
|
+
}
|
|
85
|
+
function activateFreeTier() {
|
|
86
|
+
const deviceId = getDeviceId();
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
const trialEnd = now + TRIAL_DURATION_MS;
|
|
89
|
+
const freeKey = `trial_${deviceId}_${now}`;
|
|
90
|
+
// Deactivate any existing license
|
|
91
|
+
db.prepare('DELETE FROM license').run();
|
|
92
|
+
// Insert trial (7 days)
|
|
93
|
+
db.prepare(`
|
|
94
|
+
INSERT INTO license (key, email, plan, status, activated_at, expires_at, device_id, last_validated)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
96
|
+
`).run(freeKey, null, 'free', 'active', now, trialEnd, deviceId, now);
|
|
97
|
+
}
|
|
98
|
+
function deactivateLicense() {
|
|
99
|
+
db.prepare('DELETE FROM license').run();
|
|
100
|
+
}
|
|
101
|
+
function isProUser() {
|
|
102
|
+
const license = getCurrentLicense();
|
|
103
|
+
if (!license) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Check if expired
|
|
107
|
+
if (license.expires_at && Date.now() > license.expires_at) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return license.status === 'active' && license.plan !== 'free';
|
|
111
|
+
}
|
|
112
|
+
function isTrialActive() {
|
|
113
|
+
const license = getCurrentLicense();
|
|
114
|
+
if (!license || license.plan !== 'free') {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// Check if trial expired
|
|
118
|
+
if (license.expires_at && Date.now() > license.expires_at) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
function canUseCommit() {
|
|
124
|
+
const license = getCurrentLicense();
|
|
125
|
+
// No license = start trial
|
|
126
|
+
if (!license) {
|
|
127
|
+
activateFreeTier();
|
|
128
|
+
return canUseCommit(); // Retry
|
|
129
|
+
}
|
|
130
|
+
// Pro users = unlimited
|
|
131
|
+
if (isProUser()) {
|
|
132
|
+
return { allowed: true };
|
|
133
|
+
}
|
|
134
|
+
// Trial users = check if expired
|
|
135
|
+
const usage = getTrialStatus();
|
|
136
|
+
if (usage.isExpired) {
|
|
137
|
+
return {
|
|
138
|
+
allowed: false,
|
|
139
|
+
reason: '7-day trial expired',
|
|
140
|
+
usage,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { allowed: true, usage };
|
|
144
|
+
}
|
|
145
|
+
function trackCommit() {
|
|
146
|
+
const currentMonth = getCurrentMonth();
|
|
147
|
+
const existing = db.prepare('SELECT * FROM usage WHERE month = ?').get(currentMonth);
|
|
148
|
+
if (existing) {
|
|
149
|
+
db.prepare('UPDATE usage SET commits = commits + 1 WHERE month = ?').run(currentMonth);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
db.prepare('INSERT INTO usage (month, commits) VALUES (?, 1)').run(currentMonth);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function getTrialStatus() {
|
|
156
|
+
const license = getCurrentLicense();
|
|
157
|
+
if (!license || license.plan !== 'free' || !license.expires_at) {
|
|
158
|
+
return {
|
|
159
|
+
trialStarted: 0,
|
|
160
|
+
trialEndsAt: 0,
|
|
161
|
+
daysRemaining: 0,
|
|
162
|
+
isExpired: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const daysRemaining = Math.max(0, Math.ceil((license.expires_at - now) / (24 * 60 * 60 * 1000)));
|
|
167
|
+
const isExpired = now > license.expires_at;
|
|
168
|
+
return {
|
|
169
|
+
trialStarted: license.activated_at,
|
|
170
|
+
trialEndsAt: license.expires_at,
|
|
171
|
+
daysRemaining,
|
|
172
|
+
isExpired,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function getCurrentMonth() {
|
|
176
|
+
const now = new Date();
|
|
177
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
178
|
+
}
|
|
179
|
+
function getDeviceId() {
|
|
180
|
+
// Create a unique device ID based on hostname and username
|
|
181
|
+
const hostname = os_1.default.hostname();
|
|
182
|
+
const username = os_1.default.userInfo().username;
|
|
183
|
+
return Buffer.from(`${hostname}_${username}`).toString('base64').substring(0, 16);
|
|
184
|
+
}
|
|
185
|
+
function getLicenseInfo() {
|
|
186
|
+
const license = getCurrentLicense();
|
|
187
|
+
if (!license) {
|
|
188
|
+
return 'No license';
|
|
189
|
+
}
|
|
190
|
+
if (license.plan === 'free') {
|
|
191
|
+
const usage = getTrialStatus();
|
|
192
|
+
if (usage.isExpired) {
|
|
193
|
+
return 'Trial expired';
|
|
194
|
+
}
|
|
195
|
+
return `Trial (${usage.daysRemaining} day${usage.daysRemaining === 1 ? '' : 's'} remaining)`;
|
|
196
|
+
}
|
|
197
|
+
const planName = license.plan === 'pro_monthly' ? 'Pro Monthly' : 'Pro Yearly';
|
|
198
|
+
if (license.expires_at) {
|
|
199
|
+
const daysRemaining = Math.ceil((license.expires_at - Date.now()) / (24 * 60 * 60 * 1000));
|
|
200
|
+
return `${planName} (${daysRemaining} days remaining)`;
|
|
201
|
+
}
|
|
202
|
+
return planName;
|
|
203
|
+
}
|