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
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # 🔱 Trident
2
+
3
+ A modern, web-based Git client built with Next.js. Manage your repositories, view commit history with a visual graph, and perform common Git operations through an intuitive interface.
4
+
5
+ ![](./docs/poster.jpeg)
6
+
7
+ ## Features
8
+
9
+ - **Repository Management** - Add, delete, and manage multiple local Git repositories
10
+ - **Visual Commit Graph** - Fork-style commit visualization with branch lanes
11
+ - **Branch Operations** - Create, checkout, delete, rename, rebase, and merge branches
12
+ - **Change Staging** - Stage/unstage files individually or all at once
13
+ - **Stash Support** - Stash, reapply, and delete stashed changes
14
+ - **Diff Viewer** - Syntax-highlighted diff view for reviewing changes
15
+ - **Commit History** - Browse commit history with infinite scroll and branch filtering
16
+ - **Command Palette** - Quick access to common actions via Cmd/Ctrl+K
17
+ - **Settings** - Configure default repository folder and preferences
18
+ - **Dark/Light Mode** - Theme toggle for comfortable viewing
19
+
20
+ ## Roadmap & TODO
21
+
22
+ ### Implemented
23
+
24
+ #### Repository Management
25
+ - [x] Open local repositories from a filesystem browser
26
+ - [x] List recent repositories and reopen quickly (Command Palette)
27
+ - [x] Delete repositories from the app list (without deleting files)
28
+ - [x] Shared repository/settings storage across app instances
29
+ - [x] Set repository display name
30
+ - [x] Configure default root folder for browsing repositories
31
+
32
+ #### Working Directory & Staging
33
+ - [x] View working tree and index status
34
+ - [x] Stage/unstage individual files
35
+ - [x] Stage all / unstage all
36
+ - [x] Discard unstaged changes and untracked files
37
+ - [x] Stash changes with message
38
+ - [x] List/apply/pop/drop stashes
39
+ - [x] Inspect stash file lists and per-file stash diffs
40
+
41
+ #### Committing
42
+ - [x] Commit with subject + optional body
43
+ - [x] Amend latest commit message (reword)
44
+
45
+ #### Branching, History & Graph
46
+ - [x] Visual commit graph with branch lanes
47
+ - [x] Branch visibility filters persisted per repository
48
+ - [x] List local/remote branches with ahead/behind tracking info
49
+ - [x] Create branch from current HEAD or selected ref
50
+ - [x] Checkout local branch
51
+ - [x] Checkout remote branch to new local tracking branch
52
+ - [x] Delete local and remote branches
53
+ - [x] Rename local branches
54
+ - [x] Rename remote branches
55
+ - [x] Merge with options (rebase-before-merge, squash, fast-forward)
56
+ - [x] Rebase onto target branch (with optional auto-stash)
57
+ - [x] Preflight conflict checks for merge/rebase
58
+ - [x] Hard/soft/mixed reset to selected commit
59
+ - [x] Cherry-pick single/multiple commits and abort cherry-pick
60
+ - [x] Commit details and per-file commit diffs (split/inline)
61
+
62
+ #### Remote Operations
63
+ - [x] Fetch default remote
64
+ - [x] Fetch specific remote
65
+ - [x] Fetch all remotes (prune)
66
+ - [x] Pull from remote branch (rebase option)
67
+ - [x] Push to remote branch (force, set-upstream, rebase-first, squash, local-tags)
68
+
69
+ #### Tags, Credentials, and Automation
70
+ - [x] Create local tag and optionally push to remote
71
+ - [x] Delete local tag
72
+ - [x] Delete remote tag
73
+ - [x] Manage credentials (GitHub/GitLab) and associate per repository
74
+ - [x] Run repository custom bash scripts from branch context menu (with live output/cancel)
75
+
76
+ #### UX & Reliability
77
+ - [x] Dark/light/system theme toggle
78
+ - [x] Image diff support and binary file detection
79
+ - [x] Large-diff protection to prevent UI freeze
80
+ - [x] Persist key UI state (sidebar/panel sizes/folders/filters)
81
+ - [x] Git error handling with actionable messages + lock file cleanup action
82
+
83
+ ### TODO
84
+ - [ ] Clone repository from URL
85
+ - [ ] Initialize a new repository
86
+ - [ ] Manage remotes (add/edit/remove remote definitions)
87
+ - [x] Revert commits
88
+ - [ ] Commit signing (GPG/SSH)
89
+ - [ ] Co-author commit support
90
+ - [ ] Interactive rebase UI
91
+ - [ ] Blame/annotate view
92
+ - [ ] Submodule management
93
+ - [ ] Git LFS workflows
94
+
95
+ ## Tech Stack
96
+
97
+ - **Framework:** [Next.js](https://nextjs.org/) 16 (App Router)
98
+ - **Language:** TypeScript
99
+ - **UI Components:** [DaisyUI](https://daisyui.com/)
100
+ - **Styling:** [Tailwind CSS](https://tailwindcss.com/) 4
101
+ - **State Management:** [TanStack Query](https://tanstack.com/query)
102
+ - **Git Operations:** [simple-git](https://github.com/steveukx/git-js)
103
+ - **Diff Rendering:** [@alexbruf/react-diff-viewer](https://www.npmjs.com/package/@alexbruf/react-diff-viewer)
104
+ - **Theming:** [next-themes](https://github.com/pacocoursey/next-themes)
105
+ - **Validation:** [Zod](https://zod.dev/)
106
+
107
+ ## Getting Started
108
+
109
+ ### Prerequisites
110
+
111
+ - Node.js 18+
112
+ - Git installed and available in PATH
113
+
114
+ ### Run with npx
115
+
116
+ ```bash
117
+ npx trident-git
118
+ ```
119
+
120
+ This launches the app on an available local port (default `3100`).
121
+ You can also pass options:
122
+
123
+ ```bash
124
+ npx trident-git --port 3200
125
+ npx trident-git --dev
126
+ ```
127
+
128
+ ### Installation
129
+
130
+ ```bash
131
+ # Clone the repository
132
+ git clone <repository-url>
133
+ cd trident
134
+
135
+ # Install dependencies
136
+ npm install
137
+
138
+ # Start the development server
139
+ npm run dev
140
+ ```
141
+
142
+ Open [http://localhost:3000](http://localhost:3000) in your browser.
143
+
144
+ ### Build for Production
145
+
146
+ ```bash
147
+ npm run build
148
+ npm start
149
+ ```
150
+
151
+ ## Usage
152
+
153
+ 1. **Add a Repository** - Click "Add Repository" on the home page and browse to select a local Git repository
154
+ 2. **View Changes** - The workspace view shows staged and unstaged changes with diff previews
155
+ 3. **Commit Changes** - Stage files and enter a commit message (Cmd/Ctrl+Enter to commit)
156
+ 4. **Browse History** - Navigate to the History tab to view the commit graph
157
+ 5. **Manage Branches** - Use the branch sidebar to switch, create, or manage branches
158
+
159
+ ## Project Structure
160
+
161
+ ```
162
+ src/
163
+ ├── app/ # Next.js App Router pages
164
+ │ ├── api/ # API routes for Git operations
165
+ │ │ ├── git/ # Git action endpoints
166
+ │ │ ├── fs/ # File system endpoints
167
+ │ │ └── repos/ # Repository management
168
+ │ └── workspace/ # Workspace pages (history, changes, stashes, settings)
169
+ ├── components/ # React components
170
+ │ ├── git/ # Git-specific components
171
+ │ │ ├── diff-view.tsx
172
+ │ │ ├── git-graph.tsx
173
+ │ │ ├── history-view.tsx
174
+ │ │ └── status-view.tsx
175
+ │ ├── layout/ # Layout components
176
+ │ └── ui/ # Reusable UI components (Radix-based)
177
+ ├── hooks/ # Custom React hooks
178
+ ├── lib/ # Utilities and services
179
+ │ ├── git.ts # Git service wrapper
180
+ │ ├── graph-utils.ts # Commit graph algorithms
181
+ │ ├── store.ts # State management
182
+ │ └── types.ts # TypeScript definitions
183
+ ```
184
+
185
+ ## Scripts
186
+
187
+ | Command | Description |
188
+ |---------|-------------|
189
+ | `npm run dev` | Start development server |
190
+ | `npm run build` | Build for production |
191
+ | `npm run start` | Start production server |
192
+ | `npm run lint` | Run ESLint |
193
+ | `npm run cli` | Start through the packaged CLI launcher |
194
+ | `npm run pack:preview` | Preview npm package contents |
195
+
196
+ ## License
197
+
198
+ MIT
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import net from "node:net";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const APP_ROOT = path.resolve(__dirname, "..");
12
+ const NEXT_BIN = path.join(APP_ROOT, "node_modules", "next", "dist", "bin", "next");
13
+ const DEFAULT_PORT = 3100;
14
+
15
+ function printHelp() {
16
+ console.log(`Usage: trident-git [options]
17
+
18
+ Options:
19
+ -p, --port <port> Port to run on (default: 3100)
20
+ --dev Run in development mode
21
+ -h, --help Show this help message
22
+ `);
23
+ }
24
+
25
+ function parseArgs(argv) {
26
+ const options = {
27
+ mode: "start",
28
+ port: undefined,
29
+ portExplicit: false,
30
+ };
31
+
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const arg = argv[i];
34
+
35
+ if (arg === "--help" || arg === "-h") {
36
+ printHelp();
37
+ process.exit(0);
38
+ }
39
+
40
+ if (arg === "--dev") {
41
+ options.mode = "dev";
42
+ continue;
43
+ }
44
+
45
+ if (arg === "--port" || arg === "-p") {
46
+ const value = argv[i + 1];
47
+ if (!value) {
48
+ throw new Error("Missing value for --port.");
49
+ }
50
+ const parsed = Number.parseInt(value, 10);
51
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
52
+ throw new Error(`Invalid port: ${value}`);
53
+ }
54
+ options.port = parsed;
55
+ options.portExplicit = true;
56
+ i += 1;
57
+ continue;
58
+ }
59
+
60
+ throw new Error(`Unknown option: ${arg}`);
61
+ }
62
+
63
+ return options;
64
+ }
65
+
66
+ function checkPortAvailable(port) {
67
+ return new Promise((resolve) => {
68
+ const server = net.createServer();
69
+
70
+ server.once("error", () => resolve(false));
71
+ server.once("listening", () => {
72
+ server.close(() => resolve(true));
73
+ });
74
+
75
+ server.listen(port, "127.0.0.1");
76
+ });
77
+ }
78
+
79
+ async function findAvailablePort(startPort, maxAttempts = 20) {
80
+ for (let i = 0; i < maxAttempts; i += 1) {
81
+ const candidate = startPort + i;
82
+ const available = await checkPortAvailable(candidate);
83
+ if (available) {
84
+ return candidate;
85
+ }
86
+ }
87
+ throw new Error(`Could not find an available port in range ${startPort}-${startPort + maxAttempts - 1}.`);
88
+ }
89
+
90
+ function runNext(args) {
91
+ return new Promise((resolve, reject) => {
92
+ const child = spawn(process.execPath, [NEXT_BIN, ...args], {
93
+ cwd: APP_ROOT,
94
+ stdio: "inherit",
95
+ env: process.env,
96
+ });
97
+
98
+ child.on("error", reject);
99
+ child.on("exit", (code, signal) => {
100
+ if (signal) {
101
+ process.kill(process.pid, signal);
102
+ return;
103
+ }
104
+ resolve(code === null ? 1 : code);
105
+ });
106
+ });
107
+ }
108
+
109
+ async function ensureBuildExists() {
110
+ const buildIdPath = path.join(APP_ROOT, ".next", "BUILD_ID");
111
+ if (fs.existsSync(buildIdPath)) {
112
+ return;
113
+ }
114
+
115
+ console.log("No production build found. Building trident-git...");
116
+ const exitCode = await runNext(["build", "--webpack"]);
117
+ if (exitCode !== 0) {
118
+ process.exit(exitCode);
119
+ }
120
+ }
121
+
122
+ async function main() {
123
+ try {
124
+ const options = parseArgs(process.argv.slice(2));
125
+ const envPort = Number.parseInt(process.env.PORT || "", 10);
126
+ const preferredPort =
127
+ options.port ??
128
+ (Number.isInteger(envPort) && envPort > 0 && envPort <= 65535 ? envPort : DEFAULT_PORT);
129
+
130
+ let port = preferredPort;
131
+ if (!options.portExplicit && !process.env.PORT) {
132
+ port = await findAvailablePort(preferredPort);
133
+ if (port !== preferredPort) {
134
+ console.log(`Port ${preferredPort} is in use. Using ${port} instead.`);
135
+ }
136
+ }
137
+
138
+ if (options.mode === "dev") {
139
+ console.log(`Starting trident-git in development mode on http://localhost:${port}`);
140
+ process.exit(await runNext(["dev", "--webpack", "-p", String(port)]));
141
+ }
142
+
143
+ await ensureBuildExists();
144
+ console.log(`Starting trident-git on http://localhost:${port}`);
145
+ process.exit(await runNext(["start", "-p", String(port)]));
146
+ } catch (error) {
147
+ const errorMessage = error instanceof Error ? error.message : String(error);
148
+ console.error(`trident-git failed to start: ${errorMessage}`);
149
+ process.exit(1);
150
+ }
151
+ }
152
+
153
+ main();
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
package/next.config.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ // Disable Turbopack for production builds as keytar is a native module
5
+ // that requires webpack's externals configuration
6
+ experimental: {
7
+ // Use webpack for server-side code
8
+ },
9
+ // Configure webpack to handle native modules
10
+ webpack: (config, { isServer }) => {
11
+ if (isServer) {
12
+ // Externalize keytar - it's a native module that should be loaded at runtime
13
+ config.externals = config.externals || [];
14
+ config.externals.push('keytar');
15
+ }
16
+ return config;
17
+ },
18
+ images: {
19
+ remotePatterns: [
20
+ {
21
+ protocol: 'https',
22
+ hostname: 'www.google.com',
23
+ pathname: '/s2/favicons',
24
+ },
25
+ ],
26
+ },
27
+ serverExternalPackages: ['keytar'],
28
+ };
29
+
30
+ export default nextConfig;
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "trident-git",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "A modern, web-based Git client.",
6
+ "bin": {
7
+ "trident-git": "bin/trident-git.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "public",
13
+ "next.config.ts",
14
+ "postcss.config.mjs",
15
+ "tsconfig.json",
16
+ "eslint.config.mjs",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "dev": "npx gfport 3100 -- next dev --webpack -p {PORT}",
21
+ "build": "next build --webpack",
22
+ "start": "next start -p 3100",
23
+ "lint": "eslint",
24
+ "cli": "node ./bin/trident-git.mjs",
25
+ "pack:preview": "npm pack --dry-run"
26
+ },
27
+ "keywords": [
28
+ "git",
29
+ "client",
30
+ "nextjs"
31
+ ],
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@alexbruf/react-diff-viewer": "^3.1.5",
35
+ "@tanstack/react-query": "^5.90.20",
36
+ "clsx": "^2.1.1",
37
+ "daisyui": "^5.5.18",
38
+ "keytar": "^7.9.0",
39
+ "next": "16.1.6",
40
+ "next-themes": "^0.4.6",
41
+ "react": "19.2.3",
42
+ "react-dom": "19.2.3",
43
+ "react-syntax-highlighter": "^16.1.0",
44
+ "simple-git": "^3.30.0",
45
+ "tailwind-merge": "^3.4.0",
46
+ "zod": "^4.3.6"
47
+ },
48
+ "devDependencies": {
49
+ "@tailwindcss/postcss": "^4",
50
+ "@types/node": "^20",
51
+ "@types/react": "^19",
52
+ "@types/react-dom": "^19",
53
+ "@types/react-syntax-highlighter": "^15.5.13",
54
+ "eslint": "^9",
55
+ "eslint-config-next": "16.1.6",
56
+ "gfport": "^1.0.1",
57
+ "tailwindcss": "^4",
58
+ "typescript": "^5"
59
+ }
60
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
Binary file
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,113 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { z } from 'zod';
3
+ import {
4
+ getAllCredentials,
5
+ createGitHubCredential,
6
+ createGitLabCredential,
7
+ updateCredential,
8
+ deleteCredential,
9
+ } from '@/lib/credentials';
10
+
11
+ // GET - List all credentials
12
+ export async function GET() {
13
+ try {
14
+ const credentials = await getAllCredentials();
15
+ return NextResponse.json(credentials);
16
+ } catch (error) {
17
+ console.error('Failed to get credentials:', error);
18
+ return NextResponse.json({ error: 'Failed to get credentials' }, { status: 500 });
19
+ }
20
+ }
21
+
22
+ // POST - Create a new credential
23
+ const createGitHubSchema = z.object({
24
+ type: z.literal('github'),
25
+ token: z.string().min(1, 'Token is required'),
26
+ });
27
+
28
+ const createGitLabSchema = z.object({
29
+ type: z.literal('gitlab'),
30
+ serverUrl: z.string().url('Invalid server URL'),
31
+ token: z.string().min(1, 'Token is required'),
32
+ });
33
+
34
+ const createSchema = z.discriminatedUnion('type', [createGitHubSchema, createGitLabSchema]);
35
+
36
+ export async function POST(request: Request) {
37
+ try {
38
+ const body = await request.json();
39
+ const data = createSchema.parse(body);
40
+
41
+ let result;
42
+ if (data.type === 'github') {
43
+ result = await createGitHubCredential(data.token);
44
+ } else {
45
+ result = await createGitLabCredential(data.serverUrl, data.token);
46
+ }
47
+
48
+ if (!result.success) {
49
+ return NextResponse.json({ error: result.error }, { status: 400 });
50
+ }
51
+
52
+ return NextResponse.json(result.credential);
53
+ } catch (error) {
54
+ if (error instanceof z.ZodError) {
55
+ return NextResponse.json({ error: error.issues[0].message }, { status: 400 });
56
+ }
57
+ console.error('Failed to create credential:', error);
58
+ return NextResponse.json({ error: 'Failed to create credential' }, { status: 500 });
59
+ }
60
+ }
61
+
62
+ // PUT - Update a credential
63
+ const updateSchema = z.object({
64
+ id: z.string().min(1, 'Credential ID is required'),
65
+ token: z.string().min(1, 'Token is required'),
66
+ });
67
+
68
+ export async function PUT(request: Request) {
69
+ try {
70
+ const body = await request.json();
71
+ const { id, token } = updateSchema.parse(body);
72
+
73
+ const result = await updateCredential(id, token);
74
+
75
+ if (!result.success) {
76
+ return NextResponse.json({ error: result.error }, { status: 400 });
77
+ }
78
+
79
+ return NextResponse.json(result.credential);
80
+ } catch (error) {
81
+ if (error instanceof z.ZodError) {
82
+ return NextResponse.json({ error: error.issues[0].message }, { status: 400 });
83
+ }
84
+ console.error('Failed to update credential:', error);
85
+ return NextResponse.json({ error: 'Failed to update credential' }, { status: 500 });
86
+ }
87
+ }
88
+
89
+ // DELETE - Delete a credential
90
+ const deleteSchema = z.object({
91
+ id: z.string().min(1, 'Credential ID is required'),
92
+ });
93
+
94
+ export async function DELETE(request: Request) {
95
+ try {
96
+ const body = await request.json();
97
+ const { id } = deleteSchema.parse(body);
98
+
99
+ const result = await deleteCredential(id);
100
+
101
+ if (!result.success) {
102
+ return NextResponse.json({ error: result.error }, { status: 400 });
103
+ }
104
+
105
+ return NextResponse.json({ success: true });
106
+ } catch (error) {
107
+ if (error instanceof z.ZodError) {
108
+ return NextResponse.json({ error: error.issues[0].message }, { status: 400 });
109
+ }
110
+ console.error('Failed to delete credential:', error);
111
+ return NextResponse.json({ error: 'Failed to delete credential' }, { status: 500 });
112
+ }
113
+ }