gitmaps 1.0.0 β†’ 1.1.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 (46) hide show
  1. package/README.md +5 -11
  2. package/app/[owner]/[repo]/page.client.tsx +5 -0
  3. package/app/[owner]/[repo]/page.tsx +6 -0
  4. package/app/[slug]/page.client.tsx +5 -0
  5. package/app/[slug]/page.tsx +6 -0
  6. package/app/api/manifest.json/route.ts +20 -0
  7. package/app/api/pwa-icon/route.ts +14 -0
  8. package/app/api/repo/clone-stream/route.ts +20 -12
  9. package/app/api/repo/imports/route.ts +21 -3
  10. package/app/api/repo/list/route.ts +30 -0
  11. package/app/api/repo/upload/route.ts +6 -9
  12. package/app/api/sw.js/route.ts +70 -0
  13. package/app/galaxy-canvas/page.client.tsx +2 -0
  14. package/app/galaxy-canvas/page.tsx +5 -0
  15. package/app/globals.css +477 -95
  16. package/app/icon.png +0 -0
  17. package/app/layout.tsx +30 -2
  18. package/app/lib/canvas-text.ts +4 -72
  19. package/app/lib/canvas.ts +1 -1
  20. package/app/lib/card-arrangement.ts +21 -7
  21. package/app/lib/card-context-menu.tsx +2 -2
  22. package/app/lib/card-groups.ts +9 -2
  23. package/app/lib/cards.tsx +3 -1
  24. package/app/lib/connections.tsx +34 -43
  25. package/app/lib/events.tsx +25 -0
  26. package/app/lib/file-card-plugin.ts +14 -0
  27. package/app/lib/file-preview.ts +68 -41
  28. package/app/lib/galaxydraw-bridge.ts +5 -0
  29. package/app/lib/global-search.ts +48 -27
  30. package/app/lib/layers.tsx +17 -18
  31. package/app/lib/perf-overlay.ts +78 -0
  32. package/app/lib/positions.ts +1 -1
  33. package/app/lib/repo.tsx +18 -8
  34. package/app/lib/shortcuts-panel.ts +2 -0
  35. package/app/lib/viewport-culling.ts +7 -0
  36. package/app/page.client.tsx +72 -18
  37. package/app/page.tsx +22 -86
  38. package/banner.png +0 -0
  39. package/package.json +2 -2
  40. package/packages/galaxydraw/README.md +2 -2
  41. package/packages/galaxydraw/package.json +1 -1
  42. package/server.ts +1 -1
  43. package/app/api/connections/route.ts +0 -72
  44. package/app/api/positions/route.ts +0 -80
  45. package/app/api/repo/browse/route.ts +0 -55
  46. package/app/lib/pr-review.ts +0 -374
package/README.md CHANGED
@@ -11,19 +11,13 @@
11
11
 
12
12
  # πŸͺ GitMaps
13
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.
14
+ **Transcend the file tree.** GitMaps renders knowledge on an infinite canvas β€” with layers, time-travel, and a minimap to never lose context.
15
15
 
16
- 🌐 **Try it now:** [gitmaps.xyz](https://gitmaps.xyz) β€” no install required
16
+ 🌐 **Try it now:** [gitmaps.xyz](https://gitmaps.xyz) β€” no install required
17
+ πŸ”¬ **Explore React:** [gitmaps.xyz/facebook/react](https://gitmaps.xyz/facebook/react)
17
18
 
18
19
  ---
19
20
 
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
21
  ## ✨ Features
28
22
 
29
23
  | Feature | Description |
@@ -45,7 +39,7 @@ All changed files laid out on an infinite canvas. Drag them next to each other.
45
39
  | ✏️ **Full Editor** | CodeMirror 6 with syntax highlighting, multi-tab, auto-save, git commit, and code minimap. |
46
40
  | ⇄ **Tab Diff** | Compare two open tabs side-by-side with LCS diff, change markers, synced scrolling. |
47
41
  | 🧭 **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. |
42
+ | πŸ—ΊοΈ **Minimap** | Bird's-eye overview with click-to-navigate and viewport indicator. |
49
43
  | πŸ“ **PR Review** | Comment threads on any line, stored in localStorage. |
50
44
  | πŸ“ **Card Grouping** | Collapse entire directories into compact summary cards to reduce clutter. |
51
45
 
@@ -60,7 +54,7 @@ npx gitmaps
60
54
  Or clone for development:
61
55
 
62
56
  ```sh
63
- git clone https://github.com/7flash/git-on-canvas.git
57
+ git clone https://github.com/7flash/gitmaps.git
64
58
  cd galaxy-canvas
65
59
  bun install
66
60
  bun run dev
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Dynamic owner/repo route client β€” re-exports the root page client.
3
+ * This ensures the client mount script is loaded for /owner/repo URLs.
4
+ */
5
+ export { default } from '../../page.client';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Dynamic owner/repo route β€” renders the same canvas page as root.
3
+ * URL: /7flash/gitmaps, /facebook/react, etc.
4
+ * The owner and repo are read client-side from window.location.pathname.
5
+ */
6
+ export { default } from '../../page';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Dynamic slug route client β€” re-exports the root page client.
3
+ * This ensures the client mount script is loaded for /slug URLs.
4
+ */
5
+ export { default } from '../page.client';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Dynamic slug route β€” renders the same canvas page as root.
3
+ * URL: /starwar, /galaxy-canvas, etc.
4
+ * The slug is read client-side from window.location.pathname.
5
+ */
6
+ export { default } from '../page';
@@ -0,0 +1,20 @@
1
+ // GET /api/manifest.json β€” PWA Web App Manifest
2
+ export function GET() {
3
+ return Response.json({
4
+ name: 'GitMaps β€” Spatial Code Explorer',
5
+ short_name: 'GitMaps',
6
+ description: 'Transcend the file tree. Explore code on an infinite canvas with layers, time-travel, and a minimap.',
7
+ start_url: '/',
8
+ display: 'standalone',
9
+ background_color: '#0a0a0f',
10
+ theme_color: '#7c3aed',
11
+ orientation: 'any',
12
+ categories: ['developer-tools', 'productivity'],
13
+ icons: [
14
+ { src: '/api/pwa-icon?size=192', sizes: '192x192', type: 'image/png' },
15
+ { src: '/api/pwa-icon?size=512', sizes: '512x512', type: 'image/png' },
16
+ ],
17
+ }, {
18
+ headers: { 'Content-Type': 'application/manifest+json' },
19
+ });
20
+ }
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ const iconBuffer = readFileSync(join(import.meta.dir, '..', '..', 'icon.png'));
5
+
6
+ // GET /api/pwa-icon β€” serves the app icon (any size param ignored, returns PNG)
7
+ export function GET() {
8
+ return new Response(iconBuffer, {
9
+ headers: {
10
+ 'Content-Type': 'image/png',
11
+ 'Cache-Control': 'public, max-age=86400',
12
+ },
13
+ });
14
+ }
@@ -1,7 +1,7 @@
1
1
  import { measure } from 'measure-fn';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
- import { spawn } from 'child_process';
4
+
5
5
 
6
6
  const CLONES_DIR = path.join(process.cwd(), 'git-canvas', 'repos');
7
7
 
@@ -46,8 +46,8 @@ export async function POST(req: Request) {
46
46
  if (fs.existsSync(path.join(targetPath, '.git'))) {
47
47
  return measure('api:repo:clone-stream:cached', async () => {
48
48
  try {
49
- const pull = spawn('git', ['pull'], { cwd: targetPath, stdio: 'pipe' });
50
- await new Promise<void>((resolve) => pull.on('close', resolve));
49
+ const pull = Bun.spawn(['git', 'pull'], { cwd: targetPath, stdio: ['ignore', 'pipe', 'pipe'] });
50
+ await pull.exited;
51
51
  console.log(`[clone-stream] Updated existing repo: ${repoName}`);
52
52
  } catch {
53
53
  console.log(`[clone-stream] Using existing repo (pull skipped): ${repoName}`);
@@ -69,7 +69,7 @@ export async function POST(req: Request) {
69
69
 
70
70
  sendSSE('progress', { message: `Starting clone of ${repoName}...`, percent: 0 });
71
71
 
72
- const gitProc = spawn('git', ['clone', '--depth', '100', '--progress', url, targetPath], {
72
+ const gitProc = Bun.spawn(['git', 'clone', '--depth', '100', '--progress', url, targetPath], {
73
73
  stdio: ['ignore', 'pipe', 'pipe']
74
74
  });
75
75
 
@@ -99,10 +99,20 @@ export async function POST(req: Request) {
99
99
  }
100
100
  }
101
101
 
102
- gitProc.stderr.on('data', parseProgress);
103
- gitProc.stdout.on('data', parseProgress);
102
+ async function consumeStream(stream: ReadableStream) {
103
+ try {
104
+ for await (const chunk of stream) {
105
+ parseProgress(Buffer.from(chunk));
106
+ }
107
+ } catch (e) {
108
+ console.error('[clone-stream] stream parse error:', e);
109
+ }
110
+ }
104
111
 
105
- gitProc.on('close', (code) => {
112
+ if (gitProc.stderr) consumeStream(gitProc.stderr);
113
+ if (gitProc.stdout) consumeStream(gitProc.stdout);
114
+
115
+ gitProc.exited.then(code => {
106
116
  if (code === 0) {
107
117
  console.log(`[clone-stream] βœ… Cloned ${repoName}`);
108
118
  sendSSE('done', { ok: true, path: targetPath, cached: false });
@@ -110,13 +120,11 @@ export async function POST(req: Request) {
110
120
  console.error(`[clone-stream] βœ— git clone exited with code ${code}`);
111
121
  sendSSE('error', { error: `git clone failed (exit code ${code})` });
112
122
  }
113
- try { controller.close(); } catch { /* already closed */ }
114
- });
115
-
116
- gitProc.on('error', (err) => {
123
+ try { controller.close(); } catch { }
124
+ }).catch(err => {
117
125
  console.error('[clone-stream] spawn error:', err);
118
126
  sendSSE('error', { error: err.message || 'Failed to start git' });
119
- try { controller.close(); } catch { /* already closed */ }
127
+ try { controller.close(); } catch { }
120
128
  });
121
129
  }
122
130
  });
@@ -1,6 +1,7 @@
1
1
  import { measure } from 'measure-fn';
2
2
  import simpleGit from 'simple-git';
3
3
  import { validateRepoPath } from '../validate-path';
4
+ import { join } from 'path';
4
5
 
5
6
  /**
6
7
  * POST /api/repo/imports
@@ -96,13 +97,30 @@ export async function POST(req: Request) {
96
97
  });
97
98
 
98
99
  // Scan each source file for imports (limit to first 200 files for perf)
99
- const filesToScan = sourceFiles.slice(0, 200);
100
+ const filesToScan = sourceFiles.slice(0, 300);
100
101
  const edges: { source: string; target: string; line: number }[] = [];
101
102
 
103
+ const isWorkingTree = !commit || commit === 'allfiles' || commit === 'HEAD' || commit === '';
104
+
102
105
  await Promise.allSettled(filesToScan.map(async (filePath) => {
103
106
  try {
104
- const content = await git.show([`${commit}:${filePath}`]);
105
- const lines = content.split('\n');
107
+ let text = '';
108
+ if (isWorkingTree) {
109
+ try {
110
+ const file = Bun.file(join(repoPath, filePath));
111
+ if (await file.exists()) {
112
+ text = await file.text();
113
+ }
114
+ } catch {
115
+ // Fallback if failed
116
+ }
117
+ }
118
+ if (!text) {
119
+ text = await git.show([`${commit === 'allfiles' ? 'HEAD' : commit}:${filePath}`]);
120
+ }
121
+ if (!text) return;
122
+
123
+ const lines = text.split('\n');
106
124
 
107
125
  for (let i = 0; i < Math.min(lines.length, 100); i++) {
108
126
  // Only scan first 100 lines (imports are at the top)
@@ -0,0 +1,30 @@
1
+ // app/api/repo/list/route.ts β€” Lists repos from the git-canvas/repos directory
2
+ import { readdirSync, statSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ export async function GET() {
6
+ const reposDir = join(process.cwd(), 'git-canvas', 'repos');
7
+ const repos: { name: string; path: string }[] = [];
8
+
9
+ if (existsSync(reposDir)) {
10
+ try {
11
+ const entries = readdirSync(reposDir);
12
+ for (const entry of entries) {
13
+ const fullPath = join(reposDir, entry);
14
+ try {
15
+ const stat = statSync(fullPath);
16
+ if (stat.isDirectory()) {
17
+ // Check if it's a git repo (has .git)
18
+ const isGit = existsSync(join(fullPath, '.git'));
19
+ repos.push({
20
+ name: entry,
21
+ path: fullPath.replace(/\\/g, '/'),
22
+ });
23
+ }
24
+ } catch { }
25
+ }
26
+ } catch { }
27
+ }
28
+
29
+ return Response.json({ repos });
30
+ }
@@ -1,9 +1,6 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
2
  import * as path from "path";
3
- import { exec } from "child_process";
4
- import { promisify } from "util";
5
-
6
- const execAsync = promisify(exec);
3
+ import { $ } from "bun";
7
4
 
8
5
  export async function POST(req: Request) {
9
6
  try {
@@ -35,15 +32,15 @@ export async function POST(req: Request) {
35
32
  }
36
33
 
37
34
  // Initialize a Git repository so galaxy-canvas can read it
38
- await execAsync(`git init`, { cwd: repoPath });
35
+ await $`git init`.cwd(repoPath);
39
36
 
40
37
  // Setup dummy user info, otherwise git commits will fail if not globally set
41
- await execAsync(`git config user.name "Galaxy Canvas"`, { cwd: repoPath });
42
- await execAsync(`git config user.email "bot@galaxycanvas.local"`, { cwd: repoPath });
38
+ await $`git config user.name "Galaxy Canvas"`.cwd(repoPath);
39
+ await $`git config user.email "bot@galaxycanvas.local"`.cwd(repoPath);
43
40
 
44
41
  // Add all files and commit
45
- await execAsync(`git add .`, { cwd: repoPath });
46
- await execAsync(`git commit -m "Initial drop imported by drag-and-drop"`, { cwd: repoPath });
42
+ await $`git add .`.cwd(repoPath);
43
+ await $`git commit -m "Initial drop imported by drag-and-drop"`.cwd(repoPath);
47
44
 
48
45
  return Response.json({ path: repoPath, success: true });
49
46
  } catch (e: any) {
@@ -0,0 +1,70 @@
1
+ // GET /api/sw.js β€” Service Worker for offline caching
2
+ export function GET() {
3
+ const sw = `
4
+ const CACHE_NAME = 'gitmaps-v1';
5
+ const PRECACHE = [
6
+ '/',
7
+ ];
8
+
9
+ self.addEventListener('install', (e) => {
10
+ e.waitUntil(
11
+ caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE))
12
+ );
13
+ self.skipWaiting();
14
+ });
15
+
16
+ self.addEventListener('activate', (e) => {
17
+ e.waitUntil(
18
+ caches.keys().then(keys =>
19
+ Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
20
+ )
21
+ );
22
+ self.clients.claim();
23
+ });
24
+
25
+ self.addEventListener('fetch', (e) => {
26
+ const url = new URL(e.request.url);
27
+
28
+ // Skip non-GET, WebSocket, and API requests (except manifest)
29
+ if (e.request.method !== 'GET') return;
30
+ if (url.protocol === 'ws:' || url.protocol === 'wss:') return;
31
+ if (url.pathname.startsWith('/api/') && !url.pathname.includes('manifest')) return;
32
+
33
+ // Network-first for HTML pages (always get fresh content)
34
+ if (e.request.headers.get('accept')?.includes('text/html')) {
35
+ e.respondWith(
36
+ fetch(e.request)
37
+ .then(res => {
38
+ const clone = res.clone();
39
+ caches.open(CACHE_NAME).then(c => c.put(e.request, clone));
40
+ return res;
41
+ })
42
+ .catch(() => caches.match(e.request))
43
+ );
44
+ return;
45
+ }
46
+
47
+ // Cache-first for static assets (CSS, JS, fonts, images)
48
+ e.respondWith(
49
+ caches.match(e.request).then(cached => {
50
+ if (cached) return cached;
51
+ return fetch(e.request).then(res => {
52
+ if (res.ok && res.status === 200) {
53
+ const clone = res.clone();
54
+ caches.open(CACHE_NAME).then(c => c.put(e.request, clone));
55
+ }
56
+ return res;
57
+ });
58
+ })
59
+ );
60
+ });
61
+ `;
62
+
63
+ return new Response(sw, {
64
+ headers: {
65
+ 'Content-Type': 'application/javascript',
66
+ 'Service-Worker-Allowed': '/',
67
+ 'Cache-Control': 'no-cache',
68
+ },
69
+ });
70
+ }
@@ -0,0 +1,2 @@
1
+ // Re-export root page client for /galaxy-canvas route
2
+ export { default } from '../page.client';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * galaxy-canvas route β€” Aliases root page for /galaxy-canvas path
3
+ */
4
+ import Page from '../page';
5
+ export default Page;