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,266 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+ import type { Repository } from './types';
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs))
7
+ }
8
+
9
+ export function getRepoFolderName(repoPath: string): string {
10
+ const normalizedPath = repoPath.replace(/[\\/]+$/, '');
11
+ const segments = normalizedPath.split(/[/\\]/).filter(Boolean);
12
+ return segments[segments.length - 1] || repoPath;
13
+ }
14
+
15
+ export function getRepositoryDisplayName(repo: Pick<Repository, 'path' | 'name' | 'displayName'>): string {
16
+ const customName = repo.displayName?.trim();
17
+ if (customName) {
18
+ return customName;
19
+ }
20
+
21
+ if (repo.name?.trim()) {
22
+ return repo.name;
23
+ }
24
+
25
+ return getRepoFolderName(repo.path);
26
+ }
27
+
28
+ function getNormalizedExtension(filePath: string): string {
29
+ if (!filePath) return '';
30
+
31
+ const fileName = filePath.split('/').pop() || '';
32
+ const lastDotIndex = fileName.lastIndexOf('.');
33
+
34
+ if (lastDotIndex === -1 || lastDotIndex === 0) {
35
+ return fileName.toLowerCase();
36
+ }
37
+
38
+ return fileName.slice(lastDotIndex + 1).toLowerCase();
39
+ }
40
+
41
+ // Known text-based file extensions
42
+ const TEXT_EXTENSIONS = new Set([
43
+ // Programming languages
44
+ 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'mts', 'cts',
45
+ 'py', 'pyw', 'pyi', 'pyx',
46
+ 'rb', 'rake', 'gemspec',
47
+ 'java', 'kt', 'kts', 'scala', 'sc', 'groovy', 'gradle',
48
+ 'c', 'h', 'cpp', 'cc', 'cxx', 'hpp', 'hh', 'hxx', 'c++', 'h++',
49
+ 'cs', 'fs', 'fsx', 'fsi',
50
+ 'go', 'rs', 'swift', 'dart', 'zig', 'nim', 'v', 'odin',
51
+ 'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'phps',
52
+ 'pl', 'pm', 'pod', 't', 'psgi',
53
+ 'lua', 'tcl', 'r', 'R', 'jl', 'ex', 'exs', 'erl', 'hrl',
54
+ 'clj', 'cljs', 'cljc', 'edn', 'lisp', 'lsp', 'el', 'scm', 'ss', 'rkt',
55
+ 'hs', 'lhs', 'elm', 'purs', 'ml', 'mli', 'f90', 'f95', 'f03', 'f08', 'for',
56
+ 'asm', 's', 'S', 'vhd', 'vhdl', 'v', 'sv', 'svh',
57
+ 'bas', 'vb', 'vbs', 'vba',
58
+ 'pas', 'pp', 'inc', 'dpr', 'dpk',
59
+ 'sh', 'bash', 'zsh', 'fish', 'ksh', 'csh', 'tcsh', 'ps1', 'psm1', 'psd1', 'bat', 'cmd',
60
+ // Web
61
+ 'html', 'htm', 'xhtml', 'shtml',
62
+ 'css', 'scss', 'sass', 'less', 'styl', 'stylus', 'pcss', 'postcss',
63
+ 'svg', 'xml', 'xsl', 'xslt', 'xsd', 'dtd', 'rss', 'atom', 'rdf', 'wsdl', 'soap',
64
+ 'vue', 'svelte', 'astro', 'mdx',
65
+ 'hbs', 'handlebars', 'mustache', 'ejs', 'pug', 'jade', 'haml', 'slim', 'erb',
66
+ 'graphql', 'gql',
67
+ // Data & Config
68
+ 'json', 'json5', 'jsonc', 'jsonl', 'ndjson', 'geojson',
69
+ 'yaml', 'yml',
70
+ 'toml', 'ini', 'cfg', 'conf', 'config', 'properties', 'env',
71
+ 'csv', 'tsv', 'psv',
72
+ 'txt', 'text', 'log', 'out',
73
+ 'md', 'markdown', 'mdown', 'mkd', 'mkdn', 'mdwn', 'rst', 'rest', 'adoc', 'asciidoc', 'asc',
74
+ 'org', 'tex', 'latex', 'ltx', 'bib', 'sty', 'cls',
75
+ 'rtf', 'diff', 'patch',
76
+ // Version control & Build
77
+ 'gitignore', 'gitattributes', 'gitmodules', 'gitconfig',
78
+ 'dockerignore', 'dockerfile', 'containerfile',
79
+ 'editorconfig', 'prettierrc', 'eslintrc', 'babelrc', 'stylelintrc', 'browserslistrc',
80
+ 'npmrc', 'nvmrc', 'yarnrc', 'pnpmfile',
81
+ 'makefile', 'mk', 'cmake', 'make', 'mak', 'rake', 'podfile', 'gemfile', 'fastfile',
82
+ // Other
83
+ 'sql', 'plsql', 'pgsql', 'mysql', 'sqlite', 'hql', 'cql',
84
+ 'proto', 'protobuf', 'thrift', 'avro', 'capnp', 'fbs', 'flatc',
85
+ 'tf', 'tfvars', 'hcl', 'nomad', 'sentinel',
86
+ 'prisma', 'graphqls',
87
+ 'lock', // package-lock.json, yarn.lock, etc.
88
+ ]);
89
+
90
+ // Known binary file extensions
91
+ const BINARY_EXTENSIONS = new Set([
92
+ // Images
93
+ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'ico', 'icns', 'tiff', 'tif', 'webp', 'avif', 'heic', 'heif',
94
+ 'psd', 'ai', 'eps', 'raw', 'cr2', 'nef', 'orf', 'sr2', 'dng',
95
+ // Video
96
+ 'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', 'mpeg', 'mpg', '3gp', '3g2', 'ogv',
97
+ // Audio
98
+ 'mp3', 'wav', 'ogg', 'oga', 'flac', 'aac', 'm4a', 'wma', 'aiff', 'aif', 'mid', 'midi', 'opus',
99
+ // Archives
100
+ 'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'lz', 'lzma', 'lz4', 'zst', 'cab', 'iso', 'dmg',
101
+ // Executables & Libraries
102
+ 'exe', 'dll', 'so', 'dylib', 'a', 'lib', 'obj', 'o',
103
+ 'app', 'msi', 'deb', 'rpm', 'apk', 'ipa', 'pkg', 'snap', 'flatpak', 'appimage',
104
+ 'class', 'jar', 'war', 'ear', 'pyc', 'pyo', 'pyd', 'beam',
105
+ 'wasm', 'wat',
106
+ // Documents
107
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'pages', 'numbers', 'key',
108
+ // Fonts
109
+ 'ttf', 'otf', 'woff', 'woff2', 'eot', 'fon', 'fnt',
110
+ // Database
111
+ 'db', 'sqlite', 'sqlite3', 'mdb', 'accdb', 'frm', 'myd', 'myi', 'ibd',
112
+ // Other binary formats
113
+ 'bin', 'dat', 'data', 'sav', 'bak',
114
+ 'swf', 'fla',
115
+ 'blend', 'fbx', 'obj', 'stl', 'gltf', 'glb', '3ds', 'dae', 'ply',
116
+ 'sketch', 'fig', 'xd',
117
+ 'unity', 'unitypackage', 'asset', 'prefab', 'meta',
118
+ ]);
119
+
120
+ const IMAGE_EXTENSIONS = new Set([
121
+ 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'icns', 'tiff', 'tif', 'webp', 'avif', 'heic', 'heif', 'svg',
122
+ ]);
123
+
124
+ /**
125
+ * Determines file type based on extension.
126
+ * @returns 'text' | 'binary' | 'unknown'
127
+ */
128
+ export function getFileTypeByExtension(filePath: string): 'text' | 'binary' | 'unknown' {
129
+ if (!filePath) return 'unknown';
130
+
131
+ const extension = getNormalizedExtension(filePath);
132
+
133
+ if (TEXT_EXTENSIONS.has(extension)) {
134
+ return 'text';
135
+ }
136
+
137
+ if (BINARY_EXTENSIONS.has(extension)) {
138
+ return 'binary';
139
+ }
140
+
141
+ return 'unknown';
142
+ }
143
+
144
+ /**
145
+ * Check if content appears to be binary (contains null bytes or high ratio of non-printable chars).
146
+ * This is used as a fallback when extension-based detection is inconclusive.
147
+ */
148
+ export function isBinaryContent(content: string): boolean {
149
+ if (!content) return false;
150
+ // Check for null bytes - strong indicator of binary content
151
+ if (content.includes('\0')) return true;
152
+ // Check first 8KB for non-printable characters
153
+ const sample = content.slice(0, 8192);
154
+ let nonPrintable = 0;
155
+ for (let i = 0; i < sample.length; i++) {
156
+ const code = sample.charCodeAt(i);
157
+ // Allow common whitespace (tab, newline, carriage return) and printable ASCII
158
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
159
+ nonPrintable++;
160
+ }
161
+ }
162
+ // If more than 10% non-printable, likely binary
163
+ return sample.length > 0 && (nonPrintable / sample.length) > 0.1;
164
+ }
165
+
166
+ /**
167
+ * Determines if a file is binary, using extension-based detection first,
168
+ * then falling back to content analysis if extension is unknown.
169
+ */
170
+ export function isFileBinary(filePath: string, leftContent?: string, rightContent?: string): boolean {
171
+ // First, try to determine from extension
172
+ const fileType = getFileTypeByExtension(filePath);
173
+
174
+ if (fileType === 'binary') {
175
+ return true;
176
+ }
177
+
178
+ if (fileType === 'text') {
179
+ return false;
180
+ }
181
+
182
+ // Extension is unknown - fall back to content analysis
183
+ return isBinaryContent(leftContent || '') || isBinaryContent(rightContent || '');
184
+ }
185
+
186
+ export function isImageFile(filePath: string): boolean {
187
+ return IMAGE_EXTENSIONS.has(getNormalizedExtension(filePath));
188
+ }
189
+
190
+ export function getImageMimeType(filePath: string): string {
191
+ const extension = getNormalizedExtension(filePath);
192
+
193
+ switch (extension) {
194
+ case 'jpg':
195
+ case 'jpeg':
196
+ return 'image/jpeg';
197
+ case 'png':
198
+ return 'image/png';
199
+ case 'gif':
200
+ return 'image/gif';
201
+ case 'bmp':
202
+ return 'image/bmp';
203
+ case 'ico':
204
+ return 'image/x-icon';
205
+ case 'icns':
206
+ return 'image/icns';
207
+ case 'tiff':
208
+ case 'tif':
209
+ return 'image/tiff';
210
+ case 'webp':
211
+ return 'image/webp';
212
+ case 'avif':
213
+ return 'image/avif';
214
+ case 'heic':
215
+ return 'image/heic';
216
+ case 'heif':
217
+ return 'image/heif';
218
+ case 'svg':
219
+ return 'image/svg+xml';
220
+ default:
221
+ return 'application/octet-stream';
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Counts changed lines in a unified git patch.
227
+ * Includes added/removed lines and skips file header markers.
228
+ */
229
+ export function getChangedLineCountFromDiff(diff: string | null | undefined): number {
230
+ if (!diff) return 0;
231
+
232
+ return diff.split('\n').reduce((count, line) => {
233
+ if (line.startsWith('+++ ') || line.startsWith('--- ')) {
234
+ return count;
235
+ }
236
+
237
+ if (line.startsWith('+') || line.startsWith('-')) {
238
+ return count + 1;
239
+ }
240
+
241
+ return count;
242
+ }, 0);
243
+ }
244
+
245
+ /**
246
+ * Sanitizes a branch name by replacing illegal Git branch name characters with "-".
247
+ * Git branch names cannot contain:
248
+ * - Space, ~, ^, :, ?, *, [, \, control characters
249
+ * - Double dots (..)
250
+ * - @{ sequence
251
+ * - Leading/trailing dots or slashes
252
+ * - Consecutive slashes
253
+ */
254
+ export function sanitizeBranchName(name: string): string {
255
+ const sanitized = name
256
+ // Replace illegal characters with "-"
257
+ .replace(/[\s~^:?*\[\]\\@{}<>|"'`!#$%&()+=;,]/g, '-')
258
+ // Replace double dots with single dash
259
+ .replace(/\.{2,}/g, '-')
260
+ // Replace consecutive slashes with single slash
261
+ .replace(/\/{2,}/g, '/')
262
+ // Replace consecutive dashes with single dash
263
+ .replace(/-{2,}/g, '-');
264
+
265
+ return sanitized;
266
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }