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.
- package/README.md +198 -0
- package/bin/trident-git.mjs +153 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +30 -0
- package/package.json +60 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/credentials/route.ts +113 -0
- package/src/app/api/custom-scripts/route.ts +203 -0
- package/src/app/api/fs/route.ts +75 -0
- package/src/app/api/git/action/route.ts +383 -0
- package/src/app/api/git/branches/route.ts +20 -0
- package/src/app/api/git/diff/route.ts +104 -0
- package/src/app/api/git/log/route.ts +28 -0
- package/src/app/api/git/status/route.ts +28 -0
- package/src/app/api/repos/route.ts +84 -0
- package/src/app/api/settings/route.ts +37 -0
- package/src/app/credentials/page.tsx +408 -0
- package/src/app/globals.css +109 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +38 -0
- package/src/app/page.tsx +10 -0
- package/src/app/providers.tsx +21 -0
- package/src/app/workspace/changes/page.tsx +27 -0
- package/src/app/workspace/custom-scripts/page.tsx +247 -0
- package/src/app/workspace/history/page.tsx +27 -0
- package/src/app/workspace/layout.tsx +26 -0
- package/src/app/workspace/page.tsx +27 -0
- package/src/app/workspace/settings/page.tsx +233 -0
- package/src/app/workspace/stashes/page.tsx +395 -0
- package/src/components/command-palette.tsx +178 -0
- package/src/components/context-menu.tsx +200 -0
- package/src/components/fs-browser.tsx +154 -0
- package/src/components/git/diff-view.tsx +137 -0
- package/src/components/git/git-graph.tsx +489 -0
- package/src/components/git/grouped-diff-viewer.tsx +332 -0
- package/src/components/git/history-view.tsx +4862 -0
- package/src/components/git/image-diff-view.tsx +342 -0
- package/src/components/git/status-view.tsx +597 -0
- package/src/components/home-settings-modal.tsx +192 -0
- package/src/components/layout/sidebar.tsx +256 -0
- package/src/components/repo-list.tsx +206 -0
- package/src/components/theme-toggle.tsx +37 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/workspace-repo-open-tracker.tsx +39 -0
- package/src/hooks/use-credentials.ts +123 -0
- package/src/hooks/use-escape-dismiss.ts +72 -0
- package/src/hooks/use-git.ts +448 -0
- package/src/hooks/use-toast.ts +280 -0
- package/src/hooks/use-workspace-title.ts +23 -0
- package/src/lib/api-utils.ts +24 -0
- package/src/lib/branch-colors.ts +98 -0
- package/src/lib/credentials.ts +404 -0
- package/src/lib/git.ts +1510 -0
- package/src/lib/graph-utils.ts +253 -0
- package/src/lib/store.ts +145 -0
- package/src/lib/types.ts +95 -0
- package/src/lib/utils.ts +266 -0
- 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
|
+
}
|