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,104 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { GitService } from '@/lib/git';
3
+ import { getImageMimeType, isImageFile } from '@/lib/utils';
4
+ import { handleGitError } from '@/lib/api-utils';
5
+ import fs from 'node:fs';
6
+ import pathLib from 'path';
7
+
8
+ function toImageSide(buffer: Buffer | null, mimeType: string) {
9
+ if (!buffer) return null;
10
+ return {
11
+ mimeType,
12
+ base64: buffer.toString('base64'),
13
+ };
14
+ }
15
+
16
+ export async function GET(request: Request) {
17
+ const { searchParams } = new URL(request.url);
18
+ const repoPath = searchParams.get('path');
19
+ const filePath = searchParams.get('file');
20
+ const commitHash = searchParams.get('commit');
21
+
22
+ if (!repoPath) {
23
+ return NextResponse.json({ error: 'Repo path is required' }, { status: 400 });
24
+ }
25
+
26
+ // Check if path exists
27
+ if (!fs.existsSync(repoPath)) {
28
+ return NextResponse.json({ error: `Path not found: ${repoPath}` }, { status: 404 });
29
+ }
30
+
31
+ try {
32
+ const git = new GitService(repoPath);
33
+
34
+ // If commit hash is provided, get commit diff
35
+ if (commitHash) {
36
+ // If file path is also provided, get diff for that specific file in the commit
37
+ if (filePath) {
38
+ if (isImageFile(filePath)) {
39
+ const mimeType = getImageMimeType(filePath);
40
+ const [beforeBuffer, afterBuffer, diff] = await Promise.all([
41
+ git.getFileContentBuffer(filePath, `${commitHash}^`),
42
+ git.getFileContentBuffer(filePath, commitHash),
43
+ git.getCommitFilePatch(commitHash, filePath),
44
+ ]);
45
+
46
+ return NextResponse.json({
47
+ left: '',
48
+ right: '',
49
+ diff,
50
+ imageDiff: {
51
+ left: toImageSide(beforeBuffer, mimeType),
52
+ right: toImageSide(afterBuffer, mimeType),
53
+ },
54
+ });
55
+ }
56
+
57
+ const { before, after, diff } = await git.getCommitFileDiff(commitHash, filePath);
58
+ return NextResponse.json({ left: before, right: after, diff });
59
+ }
60
+
61
+ // Otherwise, get the list of files changed in the commit
62
+ const { files, diff } = await git.getCommitDiff(commitHash);
63
+ return NextResponse.json({ files, diff });
64
+ }
65
+
66
+ // Original behavior: diff against working directory
67
+ if (!filePath) {
68
+ return NextResponse.json({ error: 'File path is required' }, { status: 400 });
69
+ }
70
+
71
+ const diff = await git.getDiff(filePath);
72
+
73
+ if (isImageFile(filePath)) {
74
+ const mimeType = getImageMimeType(filePath);
75
+ const fullPath = pathLib.join(repoPath, filePath);
76
+ const [leftBuffer, rightBuffer] = await Promise.all([
77
+ git.getFileContentBuffer(filePath, 'HEAD'),
78
+ fs.existsSync(fullPath) ? fs.promises.readFile(fullPath) : Promise.resolve(null),
79
+ ]);
80
+
81
+ return NextResponse.json({
82
+ diff,
83
+ left: '',
84
+ right: '',
85
+ imageDiff: {
86
+ left: toImageSide(leftBuffer, mimeType),
87
+ right: toImageSide(rightBuffer, mimeType),
88
+ },
89
+ });
90
+ }
91
+
92
+ // Get content for Diff Viewer
93
+ const left = await git.getFileContent(filePath, 'HEAD');
94
+ let right = '';
95
+ const fullPath = pathLib.join(repoPath, filePath);
96
+ if (fs.existsSync(fullPath)) {
97
+ right = await fs.promises.readFile(fullPath, 'utf-8');
98
+ }
99
+
100
+ return NextResponse.json({ diff, left, right });
101
+ } catch (error) {
102
+ return handleGitError(error);
103
+ }
104
+ }
@@ -0,0 +1,28 @@
1
+
2
+ import { NextResponse } from 'next/server';
3
+ import { GitService } from '@/lib/git';
4
+ import { handleGitError } from '@/lib/api-utils';
5
+ import fs from 'node:fs';
6
+
7
+ export async function GET(request: Request) {
8
+ const { searchParams } = new URL(request.url);
9
+ const path = searchParams.get('path');
10
+ const limit = searchParams.get('limit');
11
+
12
+ if (!path) {
13
+ return NextResponse.json({ error: 'Repo path is required' }, { status: 400 });
14
+ }
15
+
16
+ // Check if path exists
17
+ if (!fs.existsSync(path)) {
18
+ return NextResponse.json({ error: `Path not found: ${path}` }, { status: 404 });
19
+ }
20
+
21
+ try {
22
+ const git = new GitService(path);
23
+ const log = await git.getLog(limit ? parseInt(limit) : 50);
24
+ return NextResponse.json(log);
25
+ } catch (error) {
26
+ return handleGitError(error);
27
+ }
28
+ }
@@ -0,0 +1,28 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { GitService } from '@/lib/git';
3
+ import { handleGitError } from '@/lib/api-utils';
4
+ import fs from 'node:fs/promises';
5
+
6
+ export async function GET(request: Request) {
7
+ const { searchParams } = new URL(request.url);
8
+ const path = searchParams.get('path');
9
+
10
+ if (!path) {
11
+ return NextResponse.json({ error: 'Repo path is required' }, { status: 400 });
12
+ }
13
+
14
+ // Check if path exists
15
+ try {
16
+ await fs.access(path);
17
+ } catch {
18
+ return NextResponse.json({ error: `Path not found: ${path}` }, { status: 404 });
19
+ }
20
+
21
+ try {
22
+ const git = new GitService(path);
23
+ const status = await git.getStatus();
24
+ return NextResponse.json(status);
25
+ } catch (error) {
26
+ return handleGitError(error);
27
+ }
28
+ }
@@ -0,0 +1,84 @@
1
+
2
+ import { NextResponse } from 'next/server';
3
+ import { getRepositories, addRepository, updateRepository, removeRepository } from '@/lib/store';
4
+ import { z } from 'zod';
5
+
6
+ const customScriptSchema = z.object({
7
+ id: z.string().min(1),
8
+ name: z.string().min(1),
9
+ target: z.literal('branch'),
10
+ action: z.literal('run-bash-script'),
11
+ content: z.string(),
12
+ });
13
+
14
+ export async function GET() {
15
+ const repos = getRepositories();
16
+ return NextResponse.json(repos);
17
+ }
18
+
19
+ const addRepoSchema = z.object({
20
+ path: z.string().min(1),
21
+ name: z.string().optional(),
22
+ displayName: z.string().nullable().optional(),
23
+ });
24
+
25
+ const updateRepoSchema = z.object({
26
+ path: z.string().min(1),
27
+ updates: z.object({
28
+ name: z.string().optional(),
29
+ displayName: z.string().nullable().optional(),
30
+ lastOpenedAt: z.string().optional(),
31
+ credentialId: z.string().optional().nullable(),
32
+ customScripts: z.array(customScriptSchema).optional(),
33
+ expandedFolders: z.array(z.string()).optional(),
34
+ visibilityMap: z.record(z.string(), z.enum(['visible', 'hidden'])).optional(),
35
+ localGroupExpanded: z.boolean().optional(),
36
+ remotesGroupExpanded: z.boolean().optional(),
37
+ }),
38
+ });
39
+
40
+ export async function POST(request: Request) {
41
+ try {
42
+ const body = await request.json();
43
+ const { path, name, displayName } = addRepoSchema.parse(body);
44
+ const repo = addRepository(path, name, displayName);
45
+ return NextResponse.json(repo);
46
+ } catch (error) {
47
+ if (error instanceof z.ZodError) {
48
+ return NextResponse.json({ error: error.issues }, { status: 400 });
49
+ }
50
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 });
51
+ }
52
+ }
53
+
54
+ export async function PUT(request: Request) {
55
+ try {
56
+ const body = await request.json();
57
+ const { path, updates } = updateRepoSchema.parse(body);
58
+ const repo = updateRepository(path, updates);
59
+ return NextResponse.json(repo);
60
+ } catch (error) {
61
+ if (error instanceof z.ZodError) {
62
+ return NextResponse.json({ error: error.issues }, { status: 400 });
63
+ }
64
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 });
65
+ }
66
+ }
67
+
68
+ const deleteRepoSchema = z.object({
69
+ path: z.string().min(1),
70
+ });
71
+
72
+ export async function DELETE(request: Request) {
73
+ try {
74
+ const body = await request.json();
75
+ const { path } = deleteRepoSchema.parse(body);
76
+ removeRepository(path);
77
+ return NextResponse.json({ success: true });
78
+ } catch (error) {
79
+ if (error instanceof z.ZodError) {
80
+ return NextResponse.json({ error: error.issues }, { status: 400 });
81
+ }
82
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 });
83
+ }
84
+ }
@@ -0,0 +1,37 @@
1
+
2
+ import { NextResponse } from 'next/server';
3
+ import { getSettings, updateSettings, getDefaultRootFolder } from '@/lib/store';
4
+ import { z } from 'zod';
5
+
6
+ export async function GET() {
7
+ const settings = getSettings();
8
+ const resolvedDefaultFolder = getDefaultRootFolder();
9
+ return NextResponse.json({
10
+ ...settings,
11
+ resolvedDefaultFolder, // The actual folder that will be used (after fallback logic)
12
+ });
13
+ }
14
+
15
+ const updateSettingsSchema = z.object({
16
+ defaultRootFolder: z.string().nullable().optional(),
17
+ sidebarCollapsed: z.boolean().optional(),
18
+ historyPanelHeight: z.number().optional(),
19
+ });
20
+
21
+ export async function PUT(request: Request) {
22
+ try {
23
+ const body = await request.json();
24
+ const updates = updateSettingsSchema.parse(body);
25
+ const settings = updateSettings(updates);
26
+ const resolvedDefaultFolder = getDefaultRootFolder();
27
+ return NextResponse.json({
28
+ ...settings,
29
+ resolvedDefaultFolder,
30
+ });
31
+ } catch (error) {
32
+ if (error instanceof z.ZodError) {
33
+ return NextResponse.json({ error: error.issues }, { status: 400 });
34
+ }
35
+ return NextResponse.json({ error: (error as Error).message }, { status: 500 });
36
+ }
37
+ }