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
package/src/lib/utils.ts
ADDED
|
@@ -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
|
+
}
|