gitmaps 1.0.0 → 1.1.1
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 +265 -122
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/[owner]/[repo]/page.client.tsx +5 -0
- package/app/[slug]/page.client.tsx +5 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/manifest.json/route.ts +20 -0
- package/app/api/og-image/route.ts +14 -0
- package/app/api/pwa-icon/route.ts +14 -0
- package/app/api/repo/clone-stream/route.ts +20 -12
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/imports/route.ts +21 -3
- package/app/api/repo/list/route.ts +30 -0
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/repo/upload/route.ts +6 -9
- package/app/api/sw.js/route.ts +70 -0
- package/app/api/version/route.ts +26 -0
- package/app/galaxy-canvas/page.client.tsx +2 -0
- package/app/galaxy-canvas/page.tsx +5 -0
- package/app/globals.css +5844 -4694
- package/app/icon.png +0 -0
- package/app/layout.tsx +1284 -467
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas-text.ts +4 -72
- package/app/lib/canvas.ts +625 -564
- package/app/lib/card-arrangement.ts +21 -7
- package/app/lib/card-context-menu.tsx +2 -2
- package/app/lib/card-groups.ts +9 -2
- package/app/lib/cards.tsx +1361 -914
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/connections.tsx +34 -43
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +76 -73
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -134
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -400
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/global-search.ts +48 -27
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layers.tsx +17 -18
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/perf-overlay.ts +78 -0
- package/app/lib/positions.ts +191 -122
- package/app/lib/recent-commits.test.ts +869 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -977
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/shortcuts-panel.ts +2 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -728
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -215
- package/app/page.tsx +27 -92
- package/app/state/machine.js +13 -0
- package/banner.png +0 -0
- package/package.json +17 -8
- package/server.ts +11 -1
- package/app/api/connections/route.ts +0 -72
- package/app/api/positions/route.ts +0 -80
- package/app/api/repo/browse/route.ts +0 -55
- package/app/lib/pr-review.ts +0 -374
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- 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 +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
|
@@ -1,50 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
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
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* POST /api/auth/positions — Save positions for a repo (Leader only)
|
|
4
|
+
*
|
|
5
|
+
* Leader/Follower enforcement:
|
|
6
|
+
* - Leaders (localhost/local network): Can read/write positions
|
|
7
|
+
* - Followers (remote/production): Read-only access
|
|
8
|
+
*
|
|
9
|
+
* This prevents unauthorized canvas modifications on production servers.
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getSessionFromRequest,
|
|
13
|
+
loadRepoPositions,
|
|
14
|
+
saveRepoPositions,
|
|
15
|
+
} from "../../../lib/auth";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect if request is from a leader (local) or follower (remote)
|
|
19
|
+
* Based on IP address - localhost and local network = leader
|
|
20
|
+
*/
|
|
21
|
+
function isLeaderRequest(req: Request): boolean {
|
|
22
|
+
const forwarded = req.headers.get("x-forwarded-for");
|
|
23
|
+
const ip =
|
|
24
|
+
forwarded?.split(",")[0]?.trim() ||
|
|
25
|
+
req.headers.get("x-real-ip") ||
|
|
26
|
+
"unknown";
|
|
27
|
+
|
|
28
|
+
// Leader: localhost, local network IPs
|
|
29
|
+
const leaderPatterns = [
|
|
30
|
+
/^127\./, // 127.0.0.1
|
|
31
|
+
/^::1$/, // IPv6 localhost
|
|
32
|
+
/^192\.168\./, // Private network
|
|
33
|
+
/^10\./, // Private network
|
|
34
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Private network
|
|
35
|
+
/^localhost$/i,
|
|
36
|
+
/^unknown$/i, // No IP header = likely local dev
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
return leaderPatterns.some((pattern) => pattern.test(ip));
|
|
40
|
+
}
|
|
9
41
|
|
|
10
42
|
export async function GET(req: Request) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
43
|
+
const url = new URL(req.url);
|
|
44
|
+
const repoUrl = url.searchParams.get("repo");
|
|
45
|
+
if (!repoUrl) {
|
|
46
|
+
return Response.json({ error: "repo param required" }, { status: 400 });
|
|
47
|
+
}
|
|
15
48
|
|
|
16
|
-
|
|
17
|
-
const repoUrl = url.searchParams.get('repo');
|
|
18
|
-
if (!repoUrl) {
|
|
19
|
-
return Response.json({ error: 'repo param required' }, { status: 400 });
|
|
20
|
-
}
|
|
49
|
+
const user = getSessionFromRequest(req);
|
|
21
50
|
|
|
22
|
-
|
|
51
|
+
// Allow unauthenticated reads (for public repos)
|
|
52
|
+
if (!user) {
|
|
53
|
+
// Try to load from guest account (userId 0) or return empty
|
|
54
|
+
const positionsJson = loadRepoPositions(0, repoUrl);
|
|
23
55
|
return Response.json({
|
|
24
|
-
|
|
25
|
-
|
|
56
|
+
positions: positionsJson ? JSON.parse(positionsJson) : null,
|
|
57
|
+
repoUrl,
|
|
58
|
+
authenticated: false,
|
|
26
59
|
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const positionsJson = loadRepoPositions(user.id, repoUrl);
|
|
63
|
+
return Response.json({
|
|
64
|
+
positions: positionsJson ? JSON.parse(positionsJson) : null,
|
|
65
|
+
repoUrl,
|
|
66
|
+
authenticated: true,
|
|
67
|
+
});
|
|
27
68
|
}
|
|
28
69
|
|
|
29
70
|
export async function POST(req: Request) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
// Enforce leader-only writes
|
|
72
|
+
const isLeader = isLeaderRequest(req);
|
|
73
|
+
if (!isLeader) {
|
|
74
|
+
return Response.json(
|
|
75
|
+
{
|
|
76
|
+
error:
|
|
77
|
+
"Write access denied: Follower mode (read-only). Run GitMaps locally to edit canvas.",
|
|
78
|
+
code: "FOLLOWER_READ_ONLY",
|
|
79
|
+
},
|
|
80
|
+
{ status: 403 },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const user = getSessionFromRequest(req);
|
|
34
85
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
repoUrl: string;
|
|
38
|
-
positions: Record<string, any>;
|
|
39
|
-
};
|
|
86
|
+
// Allow local dev without auth (guest mode - use userId 0)
|
|
87
|
+
const userId = user?.id ?? 0;
|
|
40
88
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
try {
|
|
90
|
+
const body = (await req.json()) as {
|
|
91
|
+
repoUrl: string;
|
|
92
|
+
positions: Record<string, any>;
|
|
93
|
+
};
|
|
44
94
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} catch (err: any) {
|
|
48
|
-
return Response.json({ error: err.message }, { status: 400 });
|
|
95
|
+
if (!body.repoUrl) {
|
|
96
|
+
return Response.json({ error: "repoUrl required" }, { status: 400 });
|
|
49
97
|
}
|
|
98
|
+
|
|
99
|
+
saveRepoPositions(
|
|
100
|
+
userId,
|
|
101
|
+
body.repoUrl,
|
|
102
|
+
JSON.stringify(body.positions || {}),
|
|
103
|
+
);
|
|
104
|
+
return Response.json({
|
|
105
|
+
ok: true,
|
|
106
|
+
mode: "leader",
|
|
107
|
+
syncedAt: new Date().toISOString(),
|
|
108
|
+
});
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
return Response.json({ error: err.message }, { status: 400 });
|
|
111
|
+
}
|
|
50
112
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
function runGit(args: string[]): string {
|
|
4
|
+
const repoRoot = path.resolve(import.meta.dir, '../../..');
|
|
5
|
+
const proc = Bun.spawnSync(['git', ...args], {
|
|
6
|
+
cwd: repoRoot,
|
|
7
|
+
stdout: 'pipe',
|
|
8
|
+
stderr: 'pipe',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (proc.exitCode !== 0) return '';
|
|
12
|
+
return proc.stdout.toString().trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function GET() {
|
|
16
|
+
const commit = process.env.GIT_COMMIT_HASH || runGit(['rev-parse', '--short', 'HEAD']) || 'unknown';
|
|
17
|
+
const commitDate = process.env.GIT_COMMIT_DATE || runGit(['log', '-1', '--format=%cs']) || '';
|
|
18
|
+
return Response.json({ commit, commitDate });
|
|
19
|
+
}
|
package/app/api/chat/route.ts
CHANGED
|
@@ -41,7 +41,12 @@ Help the user understand this file. You can:
|
|
|
41
41
|
- Suggest refactorings
|
|
42
42
|
- Explain the diff/changes
|
|
43
43
|
|
|
44
|
-
Be concise but thorough. Use code blocks with language tags for code examples. Reference line numbers when relevant
|
|
44
|
+
Be concise but thorough. Use code blocks with language tags for code examples. Reference line numbers when relevant.
|
|
45
|
+
|
|
46
|
+
To suggest physical refactors or file changes, ALWAYS use the following format:
|
|
47
|
+
<edit_file path="path/to/file.ts">
|
|
48
|
+
// Full replacement file content goes here
|
|
49
|
+
</edit_file>`;
|
|
45
50
|
} else if (canvasContext) {
|
|
46
51
|
systemPrompt = `You are an AI code assistant helping analyze a Git repository's commit changes.
|
|
47
52
|
|
|
@@ -51,7 +56,13 @@ CURRENT COMMIT: ${canvasContext.commitHash || 'none'} — ${canvasContext.commit
|
|
|
51
56
|
FILES ON CANVAS:
|
|
52
57
|
${(canvasContext.files || []).map(f => `- ${f.path} (${f.status})`).join('\n')}
|
|
53
58
|
|
|
54
|
-
Help the user understand the codebase, the commit changes, relationships between files, architecture patterns, and potential issues. Be concise and actionable
|
|
59
|
+
Help the user understand the codebase, the commit changes, relationships between files, architecture patterns, and potential issues. Be concise and actionable.
|
|
60
|
+
|
|
61
|
+
To suggest physical refactors or file changes, ALWAYS use the following format:
|
|
62
|
+
<edit_file path="path/to/file.ts">
|
|
63
|
+
// Full replacement file content goes here
|
|
64
|
+
</edit_file>
|
|
65
|
+
You may output multiple <edit_file> blocks in a single response to perform bulk refactoring across multiple files.`;
|
|
55
66
|
} else {
|
|
56
67
|
systemPrompt = `You are an AI code assistant. Help the user with their coding questions. Be concise and use code blocks with language tags.`;
|
|
57
68
|
}
|
|
@@ -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 ogBuffer = readFileSync(join(import.meta.dir, '..', '..', 'og-image.png'));
|
|
5
|
+
|
|
6
|
+
// GET /api/og-image — serves the Open Graph social sharing image
|
|
7
|
+
export function GET() {
|
|
8
|
+
return new Response(ogBuffer, {
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'image/png',
|
|
11
|
+
'Cache-Control': 'public, max-age=86400',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -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
|
-
|
|
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',
|
|
50
|
-
await
|
|
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',
|
|
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
|
-
|
|
103
|
-
|
|
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.
|
|
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 {
|
|
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 {
|
|
127
|
+
try { controller.close(); } catch { }
|
|
120
128
|
});
|
|
121
129
|
}
|
|
122
130
|
});
|
|
@@ -1,28 +1,81 @@
|
|
|
1
|
-
import { measure } from
|
|
2
|
-
import simpleGit from
|
|
3
|
-
import {
|
|
1
|
+
import { measure } from "measure-fn";
|
|
2
|
+
import simpleGit from "simple-git";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { validateRepoPath } from "../validate-path";
|
|
6
|
+
|
|
7
|
+
const MIME_TYPES: Record<string, string> = {
|
|
8
|
+
".png": "image/png",
|
|
9
|
+
".jpg": "image/jpeg",
|
|
10
|
+
".jpeg": "image/jpeg",
|
|
11
|
+
".gif": "image/gif",
|
|
12
|
+
".webp": "image/webp",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
".bmp": "image/bmp",
|
|
15
|
+
".ico": "image/x-icon",
|
|
16
|
+
};
|
|
4
17
|
|
|
5
18
|
export async function POST(req: Request) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
19
|
+
return measure("api:repo:file-content", async () => {
|
|
20
|
+
try {
|
|
21
|
+
const { path: repoPath, commit, filePath } = await req.json();
|
|
22
|
+
|
|
23
|
+
if (!repoPath || !commit || !filePath) {
|
|
24
|
+
return new Response(
|
|
25
|
+
"Repository path, commit, and file path are required",
|
|
26
|
+
{ status: 400 },
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const blocked = validateRepoPath(repoPath);
|
|
31
|
+
if (blocked) return blocked;
|
|
32
|
+
|
|
33
|
+
const git = simpleGit(repoPath);
|
|
34
|
+
const content = await git.show([`${commit}:${filePath}`]);
|
|
35
|
+
|
|
36
|
+
return Response.json({ content });
|
|
37
|
+
} catch (error: any) {
|
|
38
|
+
console.error("api:repo:file-content:error", error);
|
|
39
|
+
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function GET(req: Request) {
|
|
45
|
+
return measure("api:repo:file-image", async () => {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(req.url);
|
|
48
|
+
const repoPath = url.searchParams.get("path");
|
|
49
|
+
const file = url.searchParams.get("file");
|
|
50
|
+
|
|
51
|
+
if (!repoPath || !file) {
|
|
52
|
+
return new Response("Repository path and file are required", {
|
|
53
|
+
status: 400,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
9
56
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
57
|
+
const blocked = validateRepoPath(repoPath);
|
|
58
|
+
if (blocked) return blocked;
|
|
13
59
|
|
|
14
|
-
|
|
15
|
-
|
|
60
|
+
const ext = path.extname(file).toLowerCase();
|
|
61
|
+
const mimeType = MIME_TYPES[ext];
|
|
16
62
|
|
|
17
|
-
|
|
63
|
+
if (!mimeType) {
|
|
64
|
+
return new Response("Not an image file", { status: 400 });
|
|
65
|
+
}
|
|
18
66
|
|
|
19
|
-
|
|
20
|
-
|
|
67
|
+
const fullPath = path.join(repoPath, file);
|
|
68
|
+
const buffer = readFileSync(fullPath);
|
|
21
69
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
70
|
+
return new Response(buffer, {
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": mimeType,
|
|
73
|
+
"Cache-Control": "public, max-age=31536000",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
console.error("api:repo:file-image:error", error);
|
|
78
|
+
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
28
81
|
}
|
|
@@ -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,
|
|
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
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { extractCanonicalForgeSlugInfo } from './route';
|
|
3
|
+
|
|
4
|
+
describe('extractCanonicalForgeSlugInfo', () => {
|
|
5
|
+
test('parses GitHub HTTPS remote', () => {
|
|
6
|
+
expect(
|
|
7
|
+
extractCanonicalForgeSlugInfo('https://github.com/7flash/gitmaps.git')
|
|
8
|
+
).toEqual({
|
|
9
|
+
slug: '7flash/gitmaps',
|
|
10
|
+
source: 'github.com · https://github.com/7flash/gitmaps',
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('parses GitHub SSH remote', () => {
|
|
15
|
+
expect(
|
|
16
|
+
extractCanonicalForgeSlugInfo('git@github.com:7flash/gitmaps.git')
|
|
17
|
+
).toEqual({
|
|
18
|
+
slug: '7flash/gitmaps',
|
|
19
|
+
source: 'github.com · git@github.com:7flash/gitmaps',
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('preserves deep GitLab namespace', () => {
|
|
24
|
+
expect(
|
|
25
|
+
extractCanonicalForgeSlugInfo('git@gitlab.com:team/platform/tools/gitmaps.git')
|
|
26
|
+
).toEqual({
|
|
27
|
+
slug: 'team/platform/tools/gitmaps',
|
|
28
|
+
source: 'gitlab.com · git@gitlab.com:team/platform/tools/gitmaps',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('filters forge helper path segments like scm', () => {
|
|
33
|
+
expect(
|
|
34
|
+
extractCanonicalForgeSlugInfo('https://git.example.com/scm/team/gitmaps.git')
|
|
35
|
+
).toEqual({
|
|
36
|
+
slug: 'team/gitmaps',
|
|
37
|
+
source: 'git.example.com · https://git.example.com/scm/team/gitmaps',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('returns null slug for too-deep namespaces', () => {
|
|
42
|
+
expect(
|
|
43
|
+
extractCanonicalForgeSlugInfo('https://git.example.com/a/b/c/d/e/f.git')
|
|
44
|
+
).toEqual({
|
|
45
|
+
slug: null,
|
|
46
|
+
source: 'https://git.example.com/a/b/c/d/e/f',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('returns null slug for invalid segments', () => {
|
|
51
|
+
expect(
|
|
52
|
+
extractCanonicalForgeSlugInfo('https://git.example.com/team/bad:name.git')
|
|
53
|
+
).toEqual({
|
|
54
|
+
slug: null,
|
|
55
|
+
source: 'https://git.example.com/team/bad:name',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('handles missing remote gracefully', () => {
|
|
60
|
+
expect(extractCanonicalForgeSlugInfo(null)).toEqual({ slug: null, source: '' });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -3,6 +3,33 @@ import simpleGit from 'simple-git';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { validateRepoPath } from '../validate-path';
|
|
5
5
|
|
|
6
|
+
export function extractCanonicalForgeSlugInfo(remoteUrl?: string | null): { slug: string | null; source: string } {
|
|
7
|
+
if (!remoteUrl) return { slug: null, source: '' };
|
|
8
|
+
|
|
9
|
+
const normalized = remoteUrl.trim().replace(/\.git$/i, '');
|
|
10
|
+
|
|
11
|
+
const sshMatch = normalized.match(/^[^@]+@([^:]+):(.+)$/);
|
|
12
|
+
const httpsMatch = normalized.match(/^(?:https?|ssh):\/\/([^/]+)\/(.+)$/i);
|
|
13
|
+
const host = sshMatch?.[1] || httpsMatch?.[1] || '';
|
|
14
|
+
const pathPart = sshMatch?.[2] || httpsMatch?.[2];
|
|
15
|
+
if (!pathPart) return { slug: null, source: normalized };
|
|
16
|
+
|
|
17
|
+
const segments = pathPart
|
|
18
|
+
.split('/')
|
|
19
|
+
.map(s => s.trim())
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.filter(s => s !== '-' && s !== 'scm');
|
|
22
|
+
|
|
23
|
+
if (segments.length < 2) return { slug: null, source: normalized };
|
|
24
|
+
if (segments.length > 5) return { slug: null, source: normalized };
|
|
25
|
+
if (segments.some(part => /[:\\]/.test(part))) return { slug: null, source: normalized };
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
slug: segments.join('/'),
|
|
29
|
+
source: host ? `${host} · ${normalized}` : normalized,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
export async function POST(req: Request) {
|
|
7
34
|
return measure('api:repo:load', async () => {
|
|
8
35
|
try {
|
|
@@ -47,7 +74,20 @@ export async function POST(req: Request) {
|
|
|
47
74
|
refs: commit.refs ? commit.refs.split(',').map(r => r.trim()).filter(Boolean) : []
|
|
48
75
|
}));
|
|
49
76
|
|
|
50
|
-
|
|
77
|
+
let canonicalSlug: string | null = null;
|
|
78
|
+
let canonicalSlugSource = '';
|
|
79
|
+
try {
|
|
80
|
+
const remotes = await git.getRemotes(true);
|
|
81
|
+
const origin = remotes.find(r => r.name === 'origin') || remotes[0];
|
|
82
|
+
const info = extractCanonicalForgeSlugInfo(origin?.refs?.fetch || origin?.refs?.push || null);
|
|
83
|
+
canonicalSlug = info.slug;
|
|
84
|
+
canonicalSlugSource = info.source;
|
|
85
|
+
} catch {
|
|
86
|
+
canonicalSlug = null;
|
|
87
|
+
canonicalSlugSource = '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Response.json({ commits, canonicalSlug, canonicalSlugSource });
|
|
51
91
|
} catch (error: any) {
|
|
52
92
|
console.error('api:repo:load:error', error);
|
|
53
93
|
return new Response(`Error: ${error.message}`, { status: 500 });
|