gitmaps 1.0.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 +167 -0
- package/app/api/auth/favorites/route.ts +56 -0
- package/app/api/auth/github/callback/route.ts +103 -0
- package/app/api/auth/github/route.ts +32 -0
- package/app/api/auth/me/route.ts +52 -0
- package/app/api/auth/positions/route.ts +50 -0
- package/app/api/chat/route.ts +101 -0
- package/app/api/connections/route.ts +72 -0
- package/app/api/github/repos/route.ts +111 -0
- package/app/api/positions/route.ts +80 -0
- package/app/api/repo/branch-diff/route.ts +201 -0
- package/app/api/repo/branches/route.ts +53 -0
- package/app/api/repo/browse/route.ts +55 -0
- package/app/api/repo/clone/route.ts +78 -0
- package/app/api/repo/clone-stream/route.ts +131 -0
- package/app/api/repo/file-content/route.ts +28 -0
- package/app/api/repo/file-delete/route.ts +62 -0
- package/app/api/repo/file-history/route.ts +45 -0
- package/app/api/repo/file-rename/route.ts +83 -0
- package/app/api/repo/file-save/route.ts +45 -0
- package/app/api/repo/files/route.ts +169 -0
- package/app/api/repo/git-blame/route.ts +86 -0
- package/app/api/repo/git-commit/route.ts +40 -0
- package/app/api/repo/git-heatmap/route.ts +55 -0
- package/app/api/repo/imports/route.ts +154 -0
- package/app/api/repo/load/route.ts +56 -0
- package/app/api/repo/mode/route.ts +14 -0
- package/app/api/repo/search/route.ts +127 -0
- package/app/api/repo/tree/route.ts +104 -0
- package/app/api/repo/upload/route.ts +53 -0
- package/app/api/repo/validate-path.ts +53 -0
- package/app/canvas_users.db +0 -0
- package/app/canvas_users.db-shm +0 -0
- package/app/canvas_users.db-wal +0 -0
- package/app/globals.css +7899 -0
- package/app/layout.tsx +493 -0
- package/app/lib/auth.ts +193 -0
- package/app/lib/auto-save.ts +137 -0
- package/app/lib/branch-compare.ts +443 -0
- package/app/lib/breadcrumbs.ts +170 -0
- package/app/lib/canvas-export.ts +358 -0
- package/app/lib/canvas-text.ts +912 -0
- package/app/lib/canvas.ts +564 -0
- package/app/lib/card-arrangement.ts +188 -0
- package/app/lib/card-context-menu.tsx +453 -0
- package/app/lib/card-diff-markers.ts +270 -0
- package/app/lib/card-expand.ts +189 -0
- package/app/lib/card-groups.ts +246 -0
- package/app/lib/cards.tsx +914 -0
- package/app/lib/chat.tsx +308 -0
- package/app/lib/code-editor.ts +508 -0
- package/app/lib/command-palette.ts +262 -0
- package/app/lib/connections.tsx +1037 -0
- package/app/lib/context.ts +94 -0
- package/app/lib/cursor-sharing.ts +281 -0
- package/app/lib/dependency-graph.ts +438 -0
- package/app/lib/events.tsx +1747 -0
- package/app/lib/file-card-plugin.ts +134 -0
- package/app/lib/file-modal.tsx +849 -0
- package/app/lib/file-preview.ts +400 -0
- package/app/lib/file-tabs.ts +318 -0
- package/app/lib/galaxydraw-bridge.ts +477 -0
- package/app/lib/galaxydraw.test.ts +229 -0
- package/app/lib/global-search.ts +264 -0
- package/app/lib/goto-definition.ts +224 -0
- package/app/lib/heatmap.ts +178 -0
- package/app/lib/hidden-files.tsx +222 -0
- package/app/lib/layers.ts +0 -0
- package/app/lib/layers.tsx +365 -0
- package/app/lib/loading.tsx +45 -0
- package/app/lib/multi-repo.ts +286 -0
- package/app/lib/new-file-dialog.tsx +230 -0
- package/app/lib/onboarding.tsx +213 -0
- package/app/lib/perf-overlay.ts +360 -0
- package/app/lib/positions.ts +176 -0
- package/app/lib/pr-review.ts +374 -0
- package/app/lib/production-mode.ts +47 -0
- package/app/lib/repo.tsx +977 -0
- package/app/lib/settings-modal.tsx +374 -0
- package/app/lib/settings.ts +97 -0
- package/app/lib/shortcuts-panel.ts +141 -0
- package/app/lib/status-bar.ts +128 -0
- package/app/lib/symbol-outline.ts +212 -0
- package/app/lib/syntax.ts +177 -0
- package/app/lib/tab-diff.ts +238 -0
- package/app/lib/user.tsx +133 -0
- package/app/lib/utils.ts +78 -0
- package/app/lib/viewport-culling.ts +728 -0
- package/app/page.client.tsx +215 -0
- package/app/page.tsx +291 -0
- package/app/state/machine.js +196 -0
- package/app/styles/main.css +2168 -0
- package/banner.png +0 -0
- package/cli.ts +44 -0
- package/package.json +75 -0
- package/packages/galaxydraw/README.md +296 -0
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +100 -0
- package/packages/galaxydraw/demo/client.ts +154 -0
- package/packages/galaxydraw/demo/dist/client.js +8 -0
- package/packages/galaxydraw/demo/index.html +256 -0
- package/packages/galaxydraw/demo/server.ts +96 -0
- package/packages/galaxydraw/dist/index.js +984 -0
- package/packages/galaxydraw/dist/index.js.map +16 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +49 -0
- package/packages/galaxydraw/perf.test.ts +284 -0
- package/packages/galaxydraw/src/core/cards.ts +435 -0
- package/packages/galaxydraw/src/core/engine.ts +339 -0
- package/packages/galaxydraw/src/core/events.ts +81 -0
- package/packages/galaxydraw/src/core/layout.ts +136 -0
- package/packages/galaxydraw/src/core/minimap.ts +216 -0
- package/packages/galaxydraw/src/core/state.ts +177 -0
- package/packages/galaxydraw/src/core/viewport.ts +106 -0
- package/packages/galaxydraw/src/galaxydraw.css +166 -0
- package/packages/galaxydraw/src/index.ts +40 -0
- package/packages/galaxydraw/tsconfig.json +30 -0
- package/server.ts +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="banner.png" alt="GitMaps" width="100%" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="https://img.shields.io/badge/runtime-Bun-f472b6?style=flat-square" alt="Bun">
|
|
7
|
+
<img src="https://img.shields.io/badge/framework-Melina-7c3aed?style=flat-square" alt="Melina">
|
|
8
|
+
<img src="https://img.shields.io/badge/engine-GalaxyDraw-38bdf8?style=flat-square" alt="GalaxyDraw">
|
|
9
|
+
<img src="https://img.shields.io/badge/license-ISC-4ade80?style=flat-square" alt="License">
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
# 🪐 GitMaps
|
|
13
|
+
|
|
14
|
+
**See every file at once.** Pan, zoom, drag — arrange your codebase the way *you* think about it, not the way the file system forces you to.
|
|
15
|
+
|
|
16
|
+
🌐 **Try it now:** [gitmaps.xyz](https://gitmaps.xyz) — no install required
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## The Problem
|
|
21
|
+
|
|
22
|
+
Traditional code review: Open file → read → close → open next file → forget what you just saw → repeat.
|
|
23
|
+
|
|
24
|
+
**Git on Canvas:**
|
|
25
|
+
All changed files laid out on an infinite canvas. Drag them next to each other. Draw connections between related lines. Switch commits with arrow keys. Your spatial layout persists across sessions.
|
|
26
|
+
|
|
27
|
+
## ✨ Features
|
|
28
|
+
|
|
29
|
+
| Feature | Description |
|
|
30
|
+
|---------|-------------|
|
|
31
|
+
| 🖼️ **Infinite Canvas** | Pan, zoom, drag files anywhere. Your layout is saved per-commit. |
|
|
32
|
+
| 📊 **Inline Diffs** | Green additions, red deletions — right inside each card. Scrollbar markers show *where* changes are. |
|
|
33
|
+
| ⏳ **Commit Timeline** | `←` `→` arrow keys through history. Each commit shows exactly which files changed. |
|
|
34
|
+
| 📌 **Persistent Layout** | Drag files where they belong in *your* mental model. Switch commits — arrangement stays. |
|
|
35
|
+
| 🔍 **Command Palette** | `Ctrl+K` to fuzzy-search files with subsequence-weighted scoring and character-level match highlighting. |
|
|
36
|
+
| 🌿 **Branch Comparison** | Side-by-side branch diff viewer with glassmorphism drawer, status badges, and diff cards rendered on canvas. |
|
|
37
|
+
| 📦 **Multi-Repo Workspace** | Load 2-3 repos side-by-side. Auto-offset with zone labels. Sidebar tabs switch commit timelines. |
|
|
38
|
+
| 👁️ **File Preview on Hover** | Glassmorphism tooltip at low zoom: language badge, path, first 12 lines of code. |
|
|
39
|
+
| 🐙 **GitHub Import** | Paste a URL for instant clone, or search by username with live repo filtering. |
|
|
40
|
+
| 🔗 **Connections** | `Alt+click` a line → pick target file → click target line. Visual bezier curves link related code. |
|
|
41
|
+
| 📁 **Layers** | Group files into focused subsets. Each layer remembers its own viewport position. |
|
|
42
|
+
| 🤖 **AI Chat** | Press `I` to open an AI sidebar that understands your current canvas context. |
|
|
43
|
+
| ⌨️ **VIM-style Diff Nav** | `j`/`k` to jump between changes across files, `Shift+J`/`K` for file-level navigation. |
|
|
44
|
+
| 🔎 **Cross-Card Search** | Press `/` to search across all visible file cards. Results highlighted in-place with match counts. |
|
|
45
|
+
| ✏️ **Full Editor** | CodeMirror 6 with syntax highlighting, multi-tab, auto-save, git commit, and code minimap. |
|
|
46
|
+
| ⇄ **Tab Diff** | Compare two open tabs side-by-side with LCS diff, change markers, synced scrolling. |
|
|
47
|
+
| 🧭 **Symbol Outline** | Side panel showing all functions/classes/types in a file. Click to scroll. |
|
|
48
|
+
| 🔀 **Dependency Graph** | `Ctrl+G` to toggle force-directed graph layout based on import relationships. |
|
|
49
|
+
| 📝 **PR Review** | Comment threads on any line, stored in localStorage. |
|
|
50
|
+
| 📁 **Card Grouping** | Collapse entire directories into compact summary cards to reduce clutter. |
|
|
51
|
+
|
|
52
|
+
## 🚀 Quick Start
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
# One-liner (requires Bun)
|
|
56
|
+
npx gitmaps
|
|
57
|
+
# → http://localhost:3335
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or clone for development:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
git clone https://github.com/7flash/git-on-canvas.git
|
|
64
|
+
cd galaxy-canvas
|
|
65
|
+
bun install
|
|
66
|
+
bun run dev
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Open a repository by entering its path in the sidebar dropdown, or navigate directly:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
http://localhost:3335/#/path/to/your/repo
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or import from GitHub — click the GitHub icon, paste a repo URL, and it clones + opens instantly.
|
|
76
|
+
|
|
77
|
+
## 🖥️ Keyboard Shortcuts
|
|
78
|
+
|
|
79
|
+
| Key | Action |
|
|
80
|
+
|-----|--------|
|
|
81
|
+
| `←` `→` | Previous / next commit |
|
|
82
|
+
| `Ctrl+K` | Command palette — fuzzy file search |
|
|
83
|
+
| `/` or `Ctrl+F` | Cross-card text search |
|
|
84
|
+
| `j` / `k` | Next / previous diff hunk (VIM-style) |
|
|
85
|
+
| `Shift+J` / `Shift+K` | Next / previous changed file |
|
|
86
|
+
| `W` | Fit selected cards to screen |
|
|
87
|
+
| `H` | Arrange selected in a row |
|
|
88
|
+
| `V` | Arrange selected in a column |
|
|
89
|
+
| `G` | Arrange selected in a grid |
|
|
90
|
+
| `Ctrl+A` | Select all cards |
|
|
91
|
+
| `Del` / `Backspace` | Hide selected files |
|
|
92
|
+
| `Space+Drag` | Pan canvas |
|
|
93
|
+
| `Scroll` | Zoom in/out |
|
|
94
|
+
| `Ctrl+`/`Ctrl-` | Increase/decrease card font size |
|
|
95
|
+
| `I` | Toggle AI chat sidebar |
|
|
96
|
+
| `Alt+Click` | Start connection from clicked line |
|
|
97
|
+
| `Esc` | Cancel / deselect all |
|
|
98
|
+
| Double-click | Zoom to file |
|
|
99
|
+
|
|
100
|
+
## 📦 Multi-Repo Workspace
|
|
101
|
+
|
|
102
|
+
Load multiple repositories on the same canvas:
|
|
103
|
+
|
|
104
|
+
1. Open any repo normally
|
|
105
|
+
2. Use the sidebar dropdown to load a second repo
|
|
106
|
+
3. Repos appear side-by-side with **zone labels** (color-coded floating badges)
|
|
107
|
+
4. **Sidebar tabs** switch commit timelines between repos
|
|
108
|
+
5. Each repo auto-offsets horizontally with an 800px gap
|
|
109
|
+
|
|
110
|
+
## 🔗 Connections
|
|
111
|
+
|
|
112
|
+
Draw visual links between related code across files:
|
|
113
|
+
|
|
114
|
+
1. **Alt+click** a line number in any file card (source)
|
|
115
|
+
2. A **file picker** appears — search and select the target file
|
|
116
|
+
3. **Click a line** in the target file to complete the connection
|
|
117
|
+
|
|
118
|
+
Connection markers appear as colored dots on the left side of each card. Click a marker to jump to the other end.
|
|
119
|
+
|
|
120
|
+
## 📁 Layers
|
|
121
|
+
|
|
122
|
+
Layers let you isolate subsets of files for focused review:
|
|
123
|
+
|
|
124
|
+
- **Create**: Click `+ New Layer` in the bottom bar
|
|
125
|
+
- **Add files**: Right-click a card → "Add to Layer"
|
|
126
|
+
- **Switch**: Click any layer tab — canvas shows only that layer's files
|
|
127
|
+
- **Default**: "All Files" layer shows everything
|
|
128
|
+
|
|
129
|
+
Each layer remembers its own viewport position, so switching layers is instant context-switching.
|
|
130
|
+
|
|
131
|
+
## 🎮 GalaxyDraw Engine
|
|
132
|
+
|
|
133
|
+
The canvas is powered by **GalaxyDraw** — a zero-dependency infinite 2D canvas engine built for this project:
|
|
134
|
+
|
|
135
|
+
| Capability | Implementation |
|
|
136
|
+
|-----------|----------------|
|
|
137
|
+
| **Viewport culling** | Only creates DOM for visible cards. React repo (6833 files): 9 DOM nodes, 6824 deferred. ~35ms vs 14s. |
|
|
138
|
+
| **Zoom LOD** | Below 25%, cards render as lightweight pills with vertical file names. Smooth fade transitions. |
|
|
139
|
+
| **Throttled materialization** | Max 8 cards per frame with 150ms cooldown — no frame drops. |
|
|
140
|
+
| **Dual control modes** | Simple (drag=pan, scroll=zoom) or Advanced (space+drag=pan, rect select). |
|
|
141
|
+
| **Touch support** | Single-finger pan + pinch-to-zoom on tablets. |
|
|
142
|
+
| **Minimap** | Shows all files including deferred cards. Click to navigate. |
|
|
143
|
+
|
|
144
|
+
GalaxyDraw is also used by [WARMAPS](https://warmaps.xyz) for its intelligence dashboard canvas.
|
|
145
|
+
|
|
146
|
+
## 🔒 Production Security
|
|
147
|
+
|
|
148
|
+
When deployed as a SaaS (`NODE_ENV=production`):
|
|
149
|
+
|
|
150
|
+
- Path traversal protection — only `git-canvas/repos/` and `.data/uploads/` are accessible
|
|
151
|
+
- Folder browser endpoint completely blocked
|
|
152
|
+
- All 7 repo API routes validate paths via `validateRepoPath()`
|
|
153
|
+
|
|
154
|
+
## ⚙️ Stack
|
|
155
|
+
|
|
156
|
+
| Component | Technology |
|
|
157
|
+
|-----------|------------|
|
|
158
|
+
| Runtime | [Bun](https://bun.sh) |
|
|
159
|
+
| Framework | [Melina](https://github.com/7flash/melina.js) v2.5 (file-based routing, SSR, hot reload) |
|
|
160
|
+
| State | [XState](https://statemachine.xyz) v5 |
|
|
161
|
+
| Database | [sqlite-zod-orm](https://github.com/7flash/measure-fn) (positions, connections, layers) |
|
|
162
|
+
| Git | [simple-git](https://github.com/steveukx/git-js) |
|
|
163
|
+
| Profiling | [measure-fn](https://github.com/7flash/measure-fn) |
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
ISC
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/favorites — Add/remove favorite repos
|
|
3
|
+
*
|
|
4
|
+
* Body: { action: 'add' | 'remove', repoUrl: string, repoName?: string }
|
|
5
|
+
* GET /api/auth/favorites — List user's favorites
|
|
6
|
+
*/
|
|
7
|
+
import { getSessionFromRequest, addFavorite, removeFavorite, getUserFavorites } from '../../../lib/auth';
|
|
8
|
+
|
|
9
|
+
export async function GET(req: Request) {
|
|
10
|
+
const user = getSessionFromRequest(req);
|
|
11
|
+
if (!user) {
|
|
12
|
+
return Response.json({ error: 'Not authenticated' }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const favorites = getUserFavorites(user.id);
|
|
16
|
+
return Response.json({
|
|
17
|
+
favorites: favorites.map((f: any) => ({
|
|
18
|
+
repoUrl: f.repoUrl,
|
|
19
|
+
repoName: f.repoName,
|
|
20
|
+
addedAt: f.addedAt,
|
|
21
|
+
})),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function POST(req: Request) {
|
|
26
|
+
const user = getSessionFromRequest(req);
|
|
27
|
+
if (!user) {
|
|
28
|
+
return Response.json({ error: 'Not authenticated' }, { status: 401 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const body = await req.json() as {
|
|
33
|
+
action: 'add' | 'remove';
|
|
34
|
+
repoUrl: string;
|
|
35
|
+
repoName?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!body.repoUrl) {
|
|
39
|
+
return Response.json({ error: 'repoUrl required' }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (body.action === 'add') {
|
|
43
|
+
const fav = addFavorite(user.id, body.repoUrl, body.repoName);
|
|
44
|
+
return Response.json({ ok: true, favorite: { repoUrl: fav.repoUrl, repoName: fav.repoName, addedAt: fav.addedAt } });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (body.action === 'remove') {
|
|
48
|
+
const removed = removeFavorite(user.id, body.repoUrl);
|
|
49
|
+
return Response.json({ ok: true, removed });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Response.json({ error: 'action must be "add" or "remove"' }, { status: 400 });
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
return Response.json({ error: err.message }, { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/auth/github/callback — GitHub OAuth callback
|
|
3
|
+
*
|
|
4
|
+
* Exchanges the authorization code for an access token,
|
|
5
|
+
* fetches the GitHub user profile, creates/updates the user,
|
|
6
|
+
* creates a session, and redirects to the app.
|
|
7
|
+
*/
|
|
8
|
+
import { findOrCreateUser, createSession, sessionCookie } from '../../../../lib/auth';
|
|
9
|
+
|
|
10
|
+
export async function GET(req: Request) {
|
|
11
|
+
const url = new URL(req.url);
|
|
12
|
+
const code = url.searchParams.get('code');
|
|
13
|
+
const state = url.searchParams.get('state');
|
|
14
|
+
|
|
15
|
+
if (!code) {
|
|
16
|
+
return new Response('Missing authorization code', { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Verify CSRF state
|
|
20
|
+
const cookie = req.headers.get('cookie') || '';
|
|
21
|
+
const stateMatch = cookie.match(/gc_oauth_state=([a-f0-9-]+)/);
|
|
22
|
+
if (!stateMatch || stateMatch[1] !== state) {
|
|
23
|
+
return new Response('Invalid OAuth state — possible CSRF attack', { status: 403 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clientId = process.env.GITHUB_CLIENT_ID;
|
|
27
|
+
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
|
|
28
|
+
|
|
29
|
+
if (!clientId || !clientSecret) {
|
|
30
|
+
return new Response('GitHub OAuth not configured', { status: 500 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// 1. Exchange code for access token
|
|
35
|
+
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
'Accept': 'application/json',
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
client_id: clientId,
|
|
43
|
+
client_secret: clientSecret,
|
|
44
|
+
code,
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const tokenData = await tokenRes.json() as { access_token?: string; error?: string };
|
|
49
|
+
if (!tokenData.access_token) {
|
|
50
|
+
console.error('[auth] Token exchange failed:', tokenData);
|
|
51
|
+
return new Response('Failed to get access token: ' + (tokenData.error || 'unknown'), { status: 400 });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Fetch GitHub user profile
|
|
55
|
+
const userRes = await fetch('https://api.github.com/user', {
|
|
56
|
+
headers: {
|
|
57
|
+
'Authorization': `Bearer ${tokenData.access_token}`,
|
|
58
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
59
|
+
'User-Agent': 'Galaxy-Canvas',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!userRes.ok) {
|
|
64
|
+
return new Response('Failed to fetch GitHub profile', { status: 502 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ghUser = await userRes.json() as {
|
|
68
|
+
id: number;
|
|
69
|
+
login: string;
|
|
70
|
+
name?: string;
|
|
71
|
+
avatar_url?: string;
|
|
72
|
+
email?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// 3. Create or update user in our DB
|
|
76
|
+
const user = findOrCreateUser({
|
|
77
|
+
id: String(ghUser.id),
|
|
78
|
+
login: ghUser.login,
|
|
79
|
+
name: ghUser.name || undefined,
|
|
80
|
+
avatar_url: ghUser.avatar_url || undefined,
|
|
81
|
+
email: ghUser.email || undefined,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 4. Create session
|
|
85
|
+
const token = createSession(user.id);
|
|
86
|
+
|
|
87
|
+
// 5. Redirect to app with session cookie
|
|
88
|
+
return new Response(null, {
|
|
89
|
+
status: 302,
|
|
90
|
+
headers: {
|
|
91
|
+
'Location': '/',
|
|
92
|
+
'Set-Cookie': [
|
|
93
|
+
sessionCookie(token),
|
|
94
|
+
// Clear OAuth state cookie
|
|
95
|
+
'gc_oauth_state=; Path=/; HttpOnly; Max-Age=0',
|
|
96
|
+
].join(', '),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
console.error('[auth] OAuth callback error:', err.message);
|
|
101
|
+
return new Response('Authentication failed: ' + err.message, { status: 500 });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/auth/github — Redirect to GitHub OAuth
|
|
3
|
+
*
|
|
4
|
+
* Initiates the GitHub OAuth flow by redirecting to GitHub's authorize URL.
|
|
5
|
+
* Requires GITHUB_CLIENT_ID env var (from GitHub OAuth App settings).
|
|
6
|
+
*/
|
|
7
|
+
export async function GET(req: Request) {
|
|
8
|
+
const clientId = process.env.GITHUB_CLIENT_ID;
|
|
9
|
+
if (!clientId) {
|
|
10
|
+
return Response.json({ error: 'GitHub OAuth not configured (set GITHUB_CLIENT_ID)' }, { status: 500 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const url = new URL(req.url);
|
|
14
|
+
const redirectUri = `${url.origin}/api/auth/github/callback`;
|
|
15
|
+
|
|
16
|
+
// Generate state for CSRF protection
|
|
17
|
+
const state = crypto.randomUUID();
|
|
18
|
+
|
|
19
|
+
const githubUrl = new URL('https://github.com/login/oauth/authorize');
|
|
20
|
+
githubUrl.searchParams.set('client_id', clientId);
|
|
21
|
+
githubUrl.searchParams.set('redirect_uri', redirectUri);
|
|
22
|
+
githubUrl.searchParams.set('scope', 'read:user user:email');
|
|
23
|
+
githubUrl.searchParams.set('state', state);
|
|
24
|
+
|
|
25
|
+
return new Response(null, {
|
|
26
|
+
status: 302,
|
|
27
|
+
headers: {
|
|
28
|
+
'Location': githubUrl.toString(),
|
|
29
|
+
'Set-Cookie': `gc_oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/auth/me — Get current user
|
|
3
|
+
* POST /api/auth/me — Logout (delete session)
|
|
4
|
+
*
|
|
5
|
+
* Returns the authenticated user's profile, favorites, and settings.
|
|
6
|
+
*/
|
|
7
|
+
import { getSessionFromRequest, deleteSession, getUserFavorites, getAllSettings } from '../../../lib/auth';
|
|
8
|
+
|
|
9
|
+
export async function GET(req: Request) {
|
|
10
|
+
const user = getSessionFromRequest(req);
|
|
11
|
+
if (!user) {
|
|
12
|
+
return Response.json({ authenticated: false });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const favorites = getUserFavorites(user.id);
|
|
16
|
+
const settings = getAllSettings(user.id);
|
|
17
|
+
|
|
18
|
+
return Response.json({
|
|
19
|
+
authenticated: true,
|
|
20
|
+
user: {
|
|
21
|
+
id: user.id,
|
|
22
|
+
username: user.username,
|
|
23
|
+
displayName: user.displayName,
|
|
24
|
+
avatarUrl: user.avatarUrl,
|
|
25
|
+
email: user.email,
|
|
26
|
+
createdAt: user.createdAt,
|
|
27
|
+
lastLoginAt: user.lastLoginAt,
|
|
28
|
+
},
|
|
29
|
+
favorites: favorites.map((f: any) => ({
|
|
30
|
+
repoUrl: f.repoUrl,
|
|
31
|
+
repoName: f.repoName,
|
|
32
|
+
addedAt: f.addedAt,
|
|
33
|
+
})),
|
|
34
|
+
settings,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function POST(req: Request) {
|
|
39
|
+
// Logout
|
|
40
|
+
const cookie = req.headers.get('cookie') || '';
|
|
41
|
+
const match = cookie.match(/gc_session=([a-f0-9]+)/);
|
|
42
|
+
if (match) {
|
|
43
|
+
deleteSession(match[1]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return new Response(null, {
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: {
|
|
49
|
+
'Set-Cookie': 'gc_session=; Path=/; HttpOnly; Max-Age=0',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/auth/positions?repo=<url> — Load saved positions for a repo
|
|
3
|
+
* POST /api/auth/positions — Save positions for a repo
|
|
4
|
+
*
|
|
5
|
+
* Enables shared repositories: each user has their own card layout
|
|
6
|
+
* for the same cloned repository.
|
|
7
|
+
*/
|
|
8
|
+
import { getSessionFromRequest, loadRepoPositions, saveRepoPositions } from '../../../lib/auth';
|
|
9
|
+
|
|
10
|
+
export async function GET(req: Request) {
|
|
11
|
+
const user = getSessionFromRequest(req);
|
|
12
|
+
if (!user) {
|
|
13
|
+
return Response.json({ authenticated: false });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const url = new URL(req.url);
|
|
17
|
+
const repoUrl = url.searchParams.get('repo');
|
|
18
|
+
if (!repoUrl) {
|
|
19
|
+
return Response.json({ error: 'repo param required' }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const positionsJson = loadRepoPositions(user.id, repoUrl);
|
|
23
|
+
return Response.json({
|
|
24
|
+
positions: positionsJson ? JSON.parse(positionsJson) : null,
|
|
25
|
+
repoUrl,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function POST(req: Request) {
|
|
30
|
+
const user = getSessionFromRequest(req);
|
|
31
|
+
if (!user) {
|
|
32
|
+
return Response.json({ error: 'Not authenticated' }, { status: 401 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const body = await req.json() as {
|
|
37
|
+
repoUrl: string;
|
|
38
|
+
positions: Record<string, any>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (!body.repoUrl) {
|
|
42
|
+
return Response.json({ error: 'repoUrl required' }, { status: 400 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
saveRepoPositions(user.id, body.repoUrl, JSON.stringify(body.positions || {}));
|
|
46
|
+
return Response.json({ ok: true });
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
return Response.json({ error: err.message }, { status: 400 });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { measure } from 'measure-fn';
|
|
3
|
+
import { streamLLM } from 'jsx-ai';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/chat
|
|
7
|
+
* Streams an AI chat response about a file or the whole canvas.
|
|
8
|
+
*
|
|
9
|
+
* Body: { messages: [{role, content}], fileContext?: {path, content}, canvasContext?: {files: [{path, status}]} }
|
|
10
|
+
*/
|
|
11
|
+
export async function POST(req: Request) {
|
|
12
|
+
return measure('api:chat', async () => {
|
|
13
|
+
try {
|
|
14
|
+
const body = await req.json();
|
|
15
|
+
const { messages, fileContext, canvasContext, model } = body;
|
|
16
|
+
|
|
17
|
+
if (!messages || !Array.isArray(messages)) {
|
|
18
|
+
return new Response('messages array is required', { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Build system prompt based on context
|
|
22
|
+
let systemPrompt = '';
|
|
23
|
+
|
|
24
|
+
if (fileContext) {
|
|
25
|
+
systemPrompt = `You are an AI code assistant analyzing a specific file in a Git repository.
|
|
26
|
+
|
|
27
|
+
FILE: ${fileContext.path}
|
|
28
|
+
STATUS: ${fileContext.status || 'unknown'}
|
|
29
|
+
|
|
30
|
+
FILE CONTENT:
|
|
31
|
+
\`\`\`
|
|
32
|
+
${fileContext.content || '(no content available)'}
|
|
33
|
+
\`\`\`
|
|
34
|
+
|
|
35
|
+
${fileContext.diff ? `DIFF (changes in current commit):\n\`\`\`diff\n${fileContext.diff}\n\`\`\`` : ''}
|
|
36
|
+
|
|
37
|
+
Help the user understand this file. You can:
|
|
38
|
+
- Explain what the code does
|
|
39
|
+
- Point out potential issues or improvements
|
|
40
|
+
- Answer questions about specific parts
|
|
41
|
+
- Suggest refactorings
|
|
42
|
+
- Explain the diff/changes
|
|
43
|
+
|
|
44
|
+
Be concise but thorough. Use code blocks with language tags for code examples. Reference line numbers when relevant.`;
|
|
45
|
+
} else if (canvasContext) {
|
|
46
|
+
systemPrompt = `You are an AI code assistant helping analyze a Git repository's commit changes.
|
|
47
|
+
|
|
48
|
+
REPOSITORY: ${canvasContext.repoPath || 'unknown'}
|
|
49
|
+
CURRENT COMMIT: ${canvasContext.commitHash || 'none'} — ${canvasContext.commitMessage || ''}
|
|
50
|
+
|
|
51
|
+
FILES ON CANVAS:
|
|
52
|
+
${(canvasContext.files || []).map(f => `- ${f.path} (${f.status})`).join('\n')}
|
|
53
|
+
|
|
54
|
+
Help the user understand the codebase, the commit changes, relationships between files, architecture patterns, and potential issues. Be concise and actionable.`;
|
|
55
|
+
} else {
|
|
56
|
+
systemPrompt = `You are an AI code assistant. Help the user with their coding questions. Be concise and use code blocks with language tags.`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build message array for streamLLM
|
|
60
|
+
const llmMessages = [
|
|
61
|
+
{ role: 'system', content: systemPrompt },
|
|
62
|
+
...messages.map(m => ({
|
|
63
|
+
role: m.role as 'user' | 'assistant',
|
|
64
|
+
content: m.content,
|
|
65
|
+
})),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const selectedModel = model || 'gemini-2.5-flash';
|
|
69
|
+
|
|
70
|
+
// Create a ReadableStream that streams chunks
|
|
71
|
+
const stream = new ReadableStream({
|
|
72
|
+
async start(controller) {
|
|
73
|
+
try {
|
|
74
|
+
for await (const chunk of streamLLM(selectedModel, llmMessages, {
|
|
75
|
+
temperature: 0.3,
|
|
76
|
+
maxTokens: 4000,
|
|
77
|
+
})) {
|
|
78
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ text: chunk })}\n\n`));
|
|
79
|
+
}
|
|
80
|
+
controller.enqueue(new TextEncoder().encode(`data: [DONE]\n\n`));
|
|
81
|
+
controller.close();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ error: err.message })}\n\n`));
|
|
84
|
+
controller.close();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return new Response(stream, {
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'text/event-stream',
|
|
92
|
+
'Cache-Control': 'no-cache',
|
|
93
|
+
'Connection': 'keep-alive',
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
console.error('api:chat:error', error);
|
|
98
|
+
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { measure } from 'measure-fn';
|
|
2
|
+
import { Database, z } from 'sqlite-zod-orm';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const dbPath = path.join(process.cwd(), 'db', 'connections_v1.sqlite');
|
|
6
|
+
|
|
7
|
+
const db = new Database(dbPath, {
|
|
8
|
+
connections: z.object({
|
|
9
|
+
conn_id: z.string(),
|
|
10
|
+
source_file: z.string(),
|
|
11
|
+
source_line_start: z.number(),
|
|
12
|
+
source_line_end: z.number(),
|
|
13
|
+
target_file: z.string(),
|
|
14
|
+
target_line_start: z.number(),
|
|
15
|
+
target_line_end: z.number(),
|
|
16
|
+
comment: z.string().default(''),
|
|
17
|
+
created_at: z.string().default(() => new Date().toISOString()),
|
|
18
|
+
}),
|
|
19
|
+
}, {
|
|
20
|
+
indexes: { connections: ['source_file', 'target_file'] },
|
|
21
|
+
reactive: false,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export async function GET() {
|
|
25
|
+
return measure('api:connections:get', async () => {
|
|
26
|
+
try {
|
|
27
|
+
const connections = db.connections.select().all();
|
|
28
|
+
return Response.json({ connections });
|
|
29
|
+
} catch (error: any) {
|
|
30
|
+
console.error('api:connections:get:error', error);
|
|
31
|
+
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function POST(req: Request) {
|
|
37
|
+
return measure('api:connections:save', async () => {
|
|
38
|
+
try {
|
|
39
|
+
const body = await req.json();
|
|
40
|
+
const { connections } = body;
|
|
41
|
+
|
|
42
|
+
if (!Array.isArray(connections)) {
|
|
43
|
+
return new Response('connections array is required', { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Delete all existing, then re-insert
|
|
47
|
+
const existing = db.connections.select().all();
|
|
48
|
+
for (const e of existing) {
|
|
49
|
+
e.delete();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const conn of connections) {
|
|
53
|
+
db.connections.insert({
|
|
54
|
+
conn_id: conn.id,
|
|
55
|
+
source_file: conn.sourceFile,
|
|
56
|
+
source_line_start: conn.sourceLineStart,
|
|
57
|
+
source_line_end: conn.sourceLineEnd,
|
|
58
|
+
target_file: conn.targetFile,
|
|
59
|
+
target_line_start: conn.targetLineStart,
|
|
60
|
+
target_line_end: conn.targetLineEnd,
|
|
61
|
+
comment: conn.comment || '',
|
|
62
|
+
created_at: conn.createdAt || new Date().toISOString(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return Response.json({ success: true, count: connections.length });
|
|
67
|
+
} catch (error: any) {
|
|
68
|
+
console.error('api:connections:save:error', error);
|
|
69
|
+
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|