trident-git 0.2.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 (64) hide show
  1. package/README.md +198 -0
  2. package/bin/trident-git.mjs +153 -0
  3. package/eslint.config.mjs +18 -0
  4. package/next.config.ts +30 -0
  5. package/package.json +60 -0
  6. package/postcss.config.mjs +7 -0
  7. package/public/favicon.png +0 -0
  8. package/public/file.svg +1 -0
  9. package/public/globe.svg +1 -0
  10. package/public/next.svg +1 -0
  11. package/public/vercel.svg +1 -0
  12. package/public/window.svg +1 -0
  13. package/src/app/api/credentials/route.ts +113 -0
  14. package/src/app/api/custom-scripts/route.ts +203 -0
  15. package/src/app/api/fs/route.ts +75 -0
  16. package/src/app/api/git/action/route.ts +383 -0
  17. package/src/app/api/git/branches/route.ts +20 -0
  18. package/src/app/api/git/diff/route.ts +104 -0
  19. package/src/app/api/git/log/route.ts +28 -0
  20. package/src/app/api/git/status/route.ts +28 -0
  21. package/src/app/api/repos/route.ts +84 -0
  22. package/src/app/api/settings/route.ts +37 -0
  23. package/src/app/credentials/page.tsx +408 -0
  24. package/src/app/globals.css +109 -0
  25. package/src/app/icon.png +0 -0
  26. package/src/app/layout.tsx +38 -0
  27. package/src/app/page.tsx +10 -0
  28. package/src/app/providers.tsx +21 -0
  29. package/src/app/workspace/changes/page.tsx +27 -0
  30. package/src/app/workspace/custom-scripts/page.tsx +247 -0
  31. package/src/app/workspace/history/page.tsx +27 -0
  32. package/src/app/workspace/layout.tsx +26 -0
  33. package/src/app/workspace/page.tsx +27 -0
  34. package/src/app/workspace/settings/page.tsx +233 -0
  35. package/src/app/workspace/stashes/page.tsx +395 -0
  36. package/src/components/command-palette.tsx +178 -0
  37. package/src/components/context-menu.tsx +200 -0
  38. package/src/components/fs-browser.tsx +154 -0
  39. package/src/components/git/diff-view.tsx +137 -0
  40. package/src/components/git/git-graph.tsx +489 -0
  41. package/src/components/git/grouped-diff-viewer.tsx +332 -0
  42. package/src/components/git/history-view.tsx +4862 -0
  43. package/src/components/git/image-diff-view.tsx +342 -0
  44. package/src/components/git/status-view.tsx +597 -0
  45. package/src/components/home-settings-modal.tsx +192 -0
  46. package/src/components/layout/sidebar.tsx +256 -0
  47. package/src/components/repo-list.tsx +206 -0
  48. package/src/components/theme-toggle.tsx +37 -0
  49. package/src/components/toaster.tsx +36 -0
  50. package/src/components/workspace-repo-open-tracker.tsx +39 -0
  51. package/src/hooks/use-credentials.ts +123 -0
  52. package/src/hooks/use-escape-dismiss.ts +72 -0
  53. package/src/hooks/use-git.ts +448 -0
  54. package/src/hooks/use-toast.ts +280 -0
  55. package/src/hooks/use-workspace-title.ts +23 -0
  56. package/src/lib/api-utils.ts +24 -0
  57. package/src/lib/branch-colors.ts +98 -0
  58. package/src/lib/credentials.ts +404 -0
  59. package/src/lib/git.ts +1510 -0
  60. package/src/lib/graph-utils.ts +253 -0
  61. package/src/lib/store.ts +145 -0
  62. package/src/lib/types.ts +95 -0
  63. package/src/lib/utils.ts +266 -0
  64. package/tsconfig.json +34 -0
@@ -0,0 +1,404 @@
1
+ import keytar from 'keytar';
2
+
3
+ // Service name for keytar storage
4
+ const SERVICE_NAME = 'trident-git-credentials';
5
+
6
+ // Credential types
7
+ export type CredentialType = 'github' | 'gitlab';
8
+
9
+ export interface BaseCredential {
10
+ id: string;
11
+ type: CredentialType;
12
+ username: string;
13
+ createdAt: string;
14
+ updatedAt: string;
15
+ }
16
+
17
+ export interface GitHubCredential extends BaseCredential {
18
+ type: 'github';
19
+ }
20
+
21
+ export interface GitLabCredential extends BaseCredential {
22
+ type: 'gitlab';
23
+ serverUrl: string;
24
+ }
25
+
26
+ export type Credential = GitHubCredential | GitLabCredential;
27
+
28
+ // Metadata stored in local JSON (without sensitive token)
29
+ export interface CredentialMetadata {
30
+ id: string;
31
+ type: CredentialType;
32
+ username: string;
33
+ serverUrl?: string; // Only for GitLab
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+
38
+ import fs from 'fs';
39
+ import os from 'os';
40
+ import path from 'path';
41
+
42
+ // Get cross-platform app data directory
43
+ function getAppDataDir(): string {
44
+ const platform = process.platform;
45
+ const homeDir = os.homedir();
46
+
47
+ if (platform === 'win32') {
48
+ // Windows: %APPDATA%\trident
49
+ return path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 'trident');
50
+ } else if (platform === 'darwin') {
51
+ // macOS: ~/Library/Application Support/trident
52
+ return path.join(homeDir, 'Library', 'Application Support', 'trident');
53
+ } else {
54
+ // Linux/others: ~/.config/trident
55
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config'), 'trident');
56
+ }
57
+ }
58
+
59
+ const DATA_DIR = getAppDataDir();
60
+ const CREDENTIALS_FILE = path.join(DATA_DIR, 'credentials.json');
61
+
62
+ // Cache variable
63
+ let _credentialsCache: CredentialMetadata[] | null = null;
64
+
65
+ // Ensure data directory exists
66
+ if (!fs.existsSync(DATA_DIR)) {
67
+ fs.mkdirSync(DATA_DIR, { recursive: true });
68
+ }
69
+
70
+ function getCredentialsMetadata(): CredentialMetadata[] {
71
+ if (_credentialsCache !== null) {
72
+ return [..._credentialsCache];
73
+ }
74
+
75
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
76
+ _credentialsCache = [];
77
+ return [];
78
+ }
79
+ try {
80
+ const data = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
81
+ _credentialsCache = JSON.parse(data);
82
+ return [..._credentialsCache!];
83
+ } catch (error) {
84
+ console.error('Failed to parse credentials.json', error);
85
+ _credentialsCache = [];
86
+ return [];
87
+ }
88
+ }
89
+
90
+ function saveCredentialsMetadata(credentials: CredentialMetadata[]): void {
91
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2));
92
+ _credentialsCache = [...credentials];
93
+ }
94
+
95
+ // Generate a unique ID
96
+ function generateId(): string {
97
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
98
+ }
99
+
100
+ // Get account name for keytar (unique per credential)
101
+ function getKeytarAccount(id: string): string {
102
+ return `credential-${id}`;
103
+ }
104
+
105
+ // GitHub API to verify token and get username
106
+ export async function verifyGitHubToken(token: string): Promise<{ valid: boolean; username?: string; error?: string }> {
107
+ try {
108
+ const response = await fetch('https://api.github.com/user', {
109
+ headers: {
110
+ 'Authorization': `Bearer ${token}`,
111
+ 'Accept': 'application/vnd.github+json',
112
+ 'X-GitHub-Api-Version': '2022-11-28',
113
+ },
114
+ });
115
+
116
+ if (!response.ok) {
117
+ if (response.status === 401) {
118
+ return { valid: false, error: 'Invalid or expired token' };
119
+ }
120
+ return { valid: false, error: `GitHub API error: ${response.status}` };
121
+ }
122
+
123
+ const data = await response.json();
124
+ return { valid: true, username: data.login };
125
+ } catch (error) {
126
+ return { valid: false, error: `Failed to connect to GitHub: ${(error as Error).message}` };
127
+ }
128
+ }
129
+
130
+ // GitLab API to verify token and get username
131
+ export async function verifyGitLabToken(serverUrl: string, token: string): Promise<{ valid: boolean; username?: string; error?: string }> {
132
+ try {
133
+ // Normalize server URL
134
+ const baseUrl = serverUrl.replace(/\/$/, '');
135
+ const response = await fetch(`${baseUrl}/api/v4/user`, {
136
+ headers: {
137
+ 'PRIVATE-TOKEN': token,
138
+ },
139
+ });
140
+
141
+ if (!response.ok) {
142
+ if (response.status === 401) {
143
+ return { valid: false, error: 'Invalid or expired token' };
144
+ }
145
+ return { valid: false, error: `GitLab API error: ${response.status}` };
146
+ }
147
+
148
+ const data = await response.json();
149
+ return { valid: true, username: data.username };
150
+ } catch (error) {
151
+ return { valid: false, error: `Failed to connect to GitLab server: ${(error as Error).message}` };
152
+ }
153
+ }
154
+
155
+ // CRUD Operations
156
+
157
+ export async function getAllCredentials(): Promise<Credential[]> {
158
+ const metadata = getCredentialsMetadata();
159
+ return metadata.map((m) => {
160
+ if (m.type === 'gitlab') {
161
+ return {
162
+ id: m.id,
163
+ type: 'gitlab' as const,
164
+ username: m.username,
165
+ serverUrl: m.serverUrl!,
166
+ createdAt: m.createdAt,
167
+ updatedAt: m.updatedAt,
168
+ };
169
+ }
170
+ return {
171
+ id: m.id,
172
+ type: 'github' as const,
173
+ username: m.username,
174
+ createdAt: m.createdAt,
175
+ updatedAt: m.updatedAt,
176
+ };
177
+ });
178
+ }
179
+
180
+ export async function getCredentialById(id: string): Promise<Credential | null> {
181
+ const credentials = await getAllCredentials();
182
+ return credentials.find((c) => c.id === id) || null;
183
+ }
184
+
185
+ export async function getCredentialToken(id: string): Promise<string | null> {
186
+ return keytar.getPassword(SERVICE_NAME, getKeytarAccount(id));
187
+ }
188
+
189
+ export async function createGitHubCredential(token: string): Promise<{ success: boolean; credential?: GitHubCredential; error?: string }> {
190
+ // Verify token first
191
+ const verification = await verifyGitHubToken(token);
192
+ if (!verification.valid || !verification.username) {
193
+ return { success: false, error: verification.error || 'Failed to verify token' };
194
+ }
195
+
196
+ // Check if GitHub credential already exists
197
+ const existing = getCredentialsMetadata();
198
+ const existingGitHub = existing.find((c) => c.type === 'github' && c.username === verification.username);
199
+ if (existingGitHub) {
200
+ return { success: false, error: `A GitHub credential for ${verification.username} already exists. Please update or delete it first.` };
201
+ }
202
+
203
+ const id = generateId();
204
+ const now = new Date().toISOString();
205
+
206
+ // Store token securely
207
+ await keytar.setPassword(SERVICE_NAME, getKeytarAccount(id), token);
208
+
209
+ // Store metadata
210
+ const metadata: CredentialMetadata = {
211
+ id,
212
+ type: 'github',
213
+ username: verification.username,
214
+ createdAt: now,
215
+ updatedAt: now,
216
+ };
217
+
218
+ existing.push(metadata);
219
+ saveCredentialsMetadata(existing);
220
+
221
+ return {
222
+ success: true,
223
+ credential: {
224
+ id,
225
+ type: 'github',
226
+ username: verification.username,
227
+ createdAt: now,
228
+ updatedAt: now,
229
+ },
230
+ };
231
+ }
232
+
233
+ export async function createGitLabCredential(serverUrl: string, token: string): Promise<{ success: boolean; credential?: GitLabCredential; error?: string }> {
234
+ // Normalize server URL
235
+ const normalizedUrl = serverUrl.replace(/\/$/, '');
236
+
237
+ // Verify token first
238
+ const verification = await verifyGitLabToken(normalizedUrl, token);
239
+ if (!verification.valid || !verification.username) {
240
+ return { success: false, error: verification.error || 'Failed to verify token' };
241
+ }
242
+
243
+ // Check if GitLab credential for this server already exists
244
+ const existing = getCredentialsMetadata();
245
+ const existingGitLab = existing.find((c) => c.type === 'gitlab' && c.serverUrl === normalizedUrl && c.username === verification.username);
246
+ if (existingGitLab) {
247
+ return { success: false, error: `A GitLab credential for ${verification.username} on ${normalizedUrl} already exists. Please update or delete it first.` };
248
+ }
249
+
250
+ const id = generateId();
251
+ const now = new Date().toISOString();
252
+
253
+ // Store token securely
254
+ await keytar.setPassword(SERVICE_NAME, getKeytarAccount(id), token);
255
+
256
+ // Store metadata
257
+ const metadata: CredentialMetadata = {
258
+ id,
259
+ type: 'gitlab',
260
+ username: verification.username,
261
+ serverUrl: normalizedUrl,
262
+ createdAt: now,
263
+ updatedAt: now,
264
+ };
265
+
266
+ existing.push(metadata);
267
+ saveCredentialsMetadata(existing);
268
+
269
+ return {
270
+ success: true,
271
+ credential: {
272
+ id,
273
+ type: 'gitlab',
274
+ username: verification.username,
275
+ serverUrl: normalizedUrl,
276
+ createdAt: now,
277
+ updatedAt: now,
278
+ },
279
+ };
280
+ }
281
+
282
+ export async function updateCredential(id: string, token: string): Promise<{ success: boolean; credential?: Credential; error?: string }> {
283
+ const metadata = getCredentialsMetadata();
284
+ const index = metadata.findIndex((c) => c.id === id);
285
+
286
+ if (index === -1) {
287
+ return { success: false, error: 'Credential not found' };
288
+ }
289
+
290
+ const existing = metadata[index];
291
+
292
+ // Verify the new token
293
+ let verification;
294
+ if (existing.type === 'github') {
295
+ verification = await verifyGitHubToken(token);
296
+ } else {
297
+ verification = await verifyGitLabToken(existing.serverUrl!, token);
298
+ }
299
+
300
+ if (!verification.valid || !verification.username) {
301
+ return { success: false, error: verification.error || 'Failed to verify token' };
302
+ }
303
+
304
+ // Update token in keytar
305
+ await keytar.setPassword(SERVICE_NAME, getKeytarAccount(id), token);
306
+
307
+ // Update metadata
308
+ const now = new Date().toISOString();
309
+ metadata[index] = {
310
+ ...existing,
311
+ username: verification.username,
312
+ updatedAt: now,
313
+ };
314
+ saveCredentialsMetadata(metadata);
315
+
316
+ if (existing.type === 'gitlab') {
317
+ return {
318
+ success: true,
319
+ credential: {
320
+ id,
321
+ type: 'gitlab',
322
+ username: verification.username,
323
+ serverUrl: existing.serverUrl!,
324
+ createdAt: existing.createdAt,
325
+ updatedAt: now,
326
+ },
327
+ };
328
+ }
329
+
330
+ return {
331
+ success: true,
332
+ credential: {
333
+ id,
334
+ type: 'github',
335
+ username: verification.username,
336
+ createdAt: existing.createdAt,
337
+ updatedAt: now,
338
+ },
339
+ };
340
+ }
341
+
342
+ export async function deleteCredential(id: string): Promise<{ success: boolean; error?: string }> {
343
+ const metadata = getCredentialsMetadata();
344
+ const index = metadata.findIndex((c) => c.id === id);
345
+
346
+ if (index === -1) {
347
+ return { success: false, error: 'Credential not found' };
348
+ }
349
+
350
+ // Delete from keytar
351
+ await keytar.deletePassword(SERVICE_NAME, getKeytarAccount(id));
352
+
353
+ // Remove from metadata
354
+ metadata.splice(index, 1);
355
+ saveCredentialsMetadata(metadata);
356
+
357
+ return { success: true };
358
+ }
359
+
360
+ // Helper to find credential for a remote URL
361
+ export async function findCredentialForRemote(remoteUrl: string): Promise<{ credential: Credential; token: string } | null> {
362
+ const credentials = await getAllCredentials();
363
+
364
+ // Check if it's a GitHub URL
365
+ if (remoteUrl.includes('github.com')) {
366
+ const githubCred = credentials.find((c) => c.type === 'github');
367
+ if (githubCred) {
368
+ const token = await getCredentialToken(githubCred.id);
369
+ if (token) {
370
+ return { credential: githubCred, token };
371
+ }
372
+ }
373
+ }
374
+
375
+ // Check GitLab servers
376
+ for (const cred of credentials) {
377
+ if (cred.type === 'gitlab') {
378
+ // Extract host from remote URL
379
+ let host: string;
380
+ try {
381
+ if (remoteUrl.startsWith('git@')) {
382
+ // SSH URL: git@gitlab.com:user/repo.git
383
+ host = remoteUrl.split('@')[1].split(':')[0];
384
+ } else {
385
+ // HTTP URL
386
+ host = new URL(remoteUrl).host;
387
+ }
388
+ } catch {
389
+ continue;
390
+ }
391
+
392
+ // Check if the credential's server URL matches
393
+ const credHost = new URL(cred.serverUrl).host;
394
+ if (host === credHost) {
395
+ const token = await getCredentialToken(cred.id);
396
+ if (token) {
397
+ return { credential: cred, token };
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ return null;
404
+ }