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.
Files changed (121) hide show
  1. package/README.md +167 -0
  2. package/app/api/auth/favorites/route.ts +56 -0
  3. package/app/api/auth/github/callback/route.ts +103 -0
  4. package/app/api/auth/github/route.ts +32 -0
  5. package/app/api/auth/me/route.ts +52 -0
  6. package/app/api/auth/positions/route.ts +50 -0
  7. package/app/api/chat/route.ts +101 -0
  8. package/app/api/connections/route.ts +72 -0
  9. package/app/api/github/repos/route.ts +111 -0
  10. package/app/api/positions/route.ts +80 -0
  11. package/app/api/repo/branch-diff/route.ts +201 -0
  12. package/app/api/repo/branches/route.ts +53 -0
  13. package/app/api/repo/browse/route.ts +55 -0
  14. package/app/api/repo/clone/route.ts +78 -0
  15. package/app/api/repo/clone-stream/route.ts +131 -0
  16. package/app/api/repo/file-content/route.ts +28 -0
  17. package/app/api/repo/file-delete/route.ts +62 -0
  18. package/app/api/repo/file-history/route.ts +45 -0
  19. package/app/api/repo/file-rename/route.ts +83 -0
  20. package/app/api/repo/file-save/route.ts +45 -0
  21. package/app/api/repo/files/route.ts +169 -0
  22. package/app/api/repo/git-blame/route.ts +86 -0
  23. package/app/api/repo/git-commit/route.ts +40 -0
  24. package/app/api/repo/git-heatmap/route.ts +55 -0
  25. package/app/api/repo/imports/route.ts +154 -0
  26. package/app/api/repo/load/route.ts +56 -0
  27. package/app/api/repo/mode/route.ts +14 -0
  28. package/app/api/repo/search/route.ts +127 -0
  29. package/app/api/repo/tree/route.ts +104 -0
  30. package/app/api/repo/upload/route.ts +53 -0
  31. package/app/api/repo/validate-path.ts +53 -0
  32. package/app/canvas_users.db +0 -0
  33. package/app/canvas_users.db-shm +0 -0
  34. package/app/canvas_users.db-wal +0 -0
  35. package/app/globals.css +7899 -0
  36. package/app/layout.tsx +493 -0
  37. package/app/lib/auth.ts +193 -0
  38. package/app/lib/auto-save.ts +137 -0
  39. package/app/lib/branch-compare.ts +443 -0
  40. package/app/lib/breadcrumbs.ts +170 -0
  41. package/app/lib/canvas-export.ts +358 -0
  42. package/app/lib/canvas-text.ts +912 -0
  43. package/app/lib/canvas.ts +564 -0
  44. package/app/lib/card-arrangement.ts +188 -0
  45. package/app/lib/card-context-menu.tsx +453 -0
  46. package/app/lib/card-diff-markers.ts +270 -0
  47. package/app/lib/card-expand.ts +189 -0
  48. package/app/lib/card-groups.ts +246 -0
  49. package/app/lib/cards.tsx +914 -0
  50. package/app/lib/chat.tsx +308 -0
  51. package/app/lib/code-editor.ts +508 -0
  52. package/app/lib/command-palette.ts +262 -0
  53. package/app/lib/connections.tsx +1037 -0
  54. package/app/lib/context.ts +94 -0
  55. package/app/lib/cursor-sharing.ts +281 -0
  56. package/app/lib/dependency-graph.ts +438 -0
  57. package/app/lib/events.tsx +1747 -0
  58. package/app/lib/file-card-plugin.ts +134 -0
  59. package/app/lib/file-modal.tsx +849 -0
  60. package/app/lib/file-preview.ts +400 -0
  61. package/app/lib/file-tabs.ts +318 -0
  62. package/app/lib/galaxydraw-bridge.ts +477 -0
  63. package/app/lib/galaxydraw.test.ts +229 -0
  64. package/app/lib/global-search.ts +264 -0
  65. package/app/lib/goto-definition.ts +224 -0
  66. package/app/lib/heatmap.ts +178 -0
  67. package/app/lib/hidden-files.tsx +222 -0
  68. package/app/lib/layers.ts +0 -0
  69. package/app/lib/layers.tsx +365 -0
  70. package/app/lib/loading.tsx +45 -0
  71. package/app/lib/multi-repo.ts +286 -0
  72. package/app/lib/new-file-dialog.tsx +230 -0
  73. package/app/lib/onboarding.tsx +213 -0
  74. package/app/lib/perf-overlay.ts +360 -0
  75. package/app/lib/positions.ts +176 -0
  76. package/app/lib/pr-review.ts +374 -0
  77. package/app/lib/production-mode.ts +47 -0
  78. package/app/lib/repo.tsx +977 -0
  79. package/app/lib/settings-modal.tsx +374 -0
  80. package/app/lib/settings.ts +97 -0
  81. package/app/lib/shortcuts-panel.ts +141 -0
  82. package/app/lib/status-bar.ts +128 -0
  83. package/app/lib/symbol-outline.ts +212 -0
  84. package/app/lib/syntax.ts +177 -0
  85. package/app/lib/tab-diff.ts +238 -0
  86. package/app/lib/user.tsx +133 -0
  87. package/app/lib/utils.ts +78 -0
  88. package/app/lib/viewport-culling.ts +728 -0
  89. package/app/page.client.tsx +215 -0
  90. package/app/page.tsx +291 -0
  91. package/app/state/machine.js +196 -0
  92. package/app/styles/main.css +2168 -0
  93. package/banner.png +0 -0
  94. package/cli.ts +44 -0
  95. package/package.json +75 -0
  96. package/packages/galaxydraw/README.md +296 -0
  97. package/packages/galaxydraw/banner.png +0 -0
  98. package/packages/galaxydraw/demo/build-static.ts +100 -0
  99. package/packages/galaxydraw/demo/client.ts +154 -0
  100. package/packages/galaxydraw/demo/dist/client.js +8 -0
  101. package/packages/galaxydraw/demo/index.html +256 -0
  102. package/packages/galaxydraw/demo/server.ts +96 -0
  103. package/packages/galaxydraw/dist/index.js +984 -0
  104. package/packages/galaxydraw/dist/index.js.map +16 -0
  105. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  106. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  109. package/packages/galaxydraw/package.json +49 -0
  110. package/packages/galaxydraw/perf.test.ts +284 -0
  111. package/packages/galaxydraw/src/core/cards.ts +435 -0
  112. package/packages/galaxydraw/src/core/engine.ts +339 -0
  113. package/packages/galaxydraw/src/core/events.ts +81 -0
  114. package/packages/galaxydraw/src/core/layout.ts +136 -0
  115. package/packages/galaxydraw/src/core/minimap.ts +216 -0
  116. package/packages/galaxydraw/src/core/state.ts +177 -0
  117. package/packages/galaxydraw/src/core/viewport.ts +106 -0
  118. package/packages/galaxydraw/src/galaxydraw.css +166 -0
  119. package/packages/galaxydraw/src/index.ts +40 -0
  120. package/packages/galaxydraw/tsconfig.json +30 -0
  121. 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
+ }