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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { validateRepoPath } from '../validate-path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GET /api/repo/pdf-thumb?path=REPO&file=FILE&page=0&width=600
|
|
7
|
+
*
|
|
8
|
+
* Converts a PDF page to a PNG thumbnail using Bun's native sharp or
|
|
9
|
+
* falls back to pdftoppm (poppler-utils) if available.
|
|
10
|
+
* Returns the image directly as image/png.
|
|
11
|
+
*/
|
|
12
|
+
export async function GET(req: Request) {
|
|
13
|
+
const url = new URL(req.url);
|
|
14
|
+
const repoPath = url.searchParams.get('path') || '';
|
|
15
|
+
const filePath = url.searchParams.get('file') || '';
|
|
16
|
+
const page = parseInt(url.searchParams.get('page') || '0', 10);
|
|
17
|
+
const width = parseInt(url.searchParams.get('width') || '800', 10);
|
|
18
|
+
|
|
19
|
+
if (!repoPath || !filePath) {
|
|
20
|
+
return new Response('path and file params required', { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const blocked = validateRepoPath(repoPath);
|
|
24
|
+
if (blocked) return blocked;
|
|
25
|
+
|
|
26
|
+
// Prevent path traversal
|
|
27
|
+
if (filePath.includes('..') || filePath.startsWith('/')) {
|
|
28
|
+
return new Response('Invalid file path', { status: 400 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const fullPath = path.join(repoPath, filePath);
|
|
32
|
+
if (!existsSync(fullPath)) {
|
|
33
|
+
return new Response('File not found', { status: 404 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cache key based on file path + modification time
|
|
37
|
+
const file = Bun.file(fullPath);
|
|
38
|
+
const cacheKey = `pdf-thumb:${fullPath}:${file.size}:p${page}:w${width}`;
|
|
39
|
+
|
|
40
|
+
// Try pdftoppm (poppler-utils) — most reliable cross-platform PDF renderer
|
|
41
|
+
try {
|
|
42
|
+
const proc = Bun.spawnSync([
|
|
43
|
+
'pdftoppm',
|
|
44
|
+
'-png',
|
|
45
|
+
'-f', String(page + 1),
|
|
46
|
+
'-l', String(page + 1),
|
|
47
|
+
'-scale-to-x', String(width),
|
|
48
|
+
'-scale-to-y', '-1',
|
|
49
|
+
'-singlefile',
|
|
50
|
+
fullPath,
|
|
51
|
+
'-',
|
|
52
|
+
], {
|
|
53
|
+
stdout: 'pipe',
|
|
54
|
+
stderr: 'pipe',
|
|
55
|
+
timeout: 15000,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (proc.exitCode === 0 && proc.stdout.length > 0) {
|
|
59
|
+
return new Response(proc.stdout, {
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'image/png',
|
|
62
|
+
'Cache-Control': 'public, max-age=86400',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// pdftoppm outputs to stdout with `-` but some versions need a prefix
|
|
68
|
+
// Try alternative invocation
|
|
69
|
+
const proc2 = Bun.spawnSync([
|
|
70
|
+
'pdftoppm',
|
|
71
|
+
'-png',
|
|
72
|
+
'-f', String(page + 1),
|
|
73
|
+
'-l', String(page + 1),
|
|
74
|
+
'-scale-to-x', String(width),
|
|
75
|
+
'-scale-to-y', '-1',
|
|
76
|
+
'-singlefile',
|
|
77
|
+
fullPath,
|
|
78
|
+
], {
|
|
79
|
+
stdout: 'pipe',
|
|
80
|
+
stderr: 'pipe',
|
|
81
|
+
timeout: 15000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (proc2.exitCode === 0 && proc2.stdout.length > 0) {
|
|
85
|
+
return new Response(proc2.stdout, {
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'image/png',
|
|
88
|
+
'Cache-Control': 'public, max-age=86400',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} catch (_) {
|
|
93
|
+
// pdftoppm not available
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback: try magick (ImageMagick)
|
|
97
|
+
try {
|
|
98
|
+
const proc = Bun.spawnSync([
|
|
99
|
+
'magick',
|
|
100
|
+
'-density', '150',
|
|
101
|
+
`${fullPath}[${page}]`,
|
|
102
|
+
'-resize', `${width}x`,
|
|
103
|
+
'png:-',
|
|
104
|
+
], {
|
|
105
|
+
stdout: 'pipe',
|
|
106
|
+
stderr: 'pipe',
|
|
107
|
+
timeout: 30000,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (proc.exitCode === 0 && proc.stdout.length > 0) {
|
|
111
|
+
return new Response(proc.stdout, {
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'image/png',
|
|
114
|
+
'Cache-Control': 'public, max-age=86400',
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} catch (_) {
|
|
119
|
+
// magick not available
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// No PDF renderer available — return a helpful placeholder
|
|
123
|
+
return new Response('PDF preview requires poppler-utils (pdftoppm) or ImageMagick (magick). Install one to enable PDF thumbnails.', {
|
|
124
|
+
status: 501,
|
|
125
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import simpleGit from 'simple-git';
|
|
4
|
+
import { extractCanonicalForgeSlugInfo } from '../load/route';
|
|
5
|
+
|
|
6
|
+
const CLONES_DIR = path.join(process.cwd(), 'git-canvas', 'repos');
|
|
7
|
+
|
|
8
|
+
async function findRepoByCanonicalSlug(slug: string): Promise<string | null> {
|
|
9
|
+
if (!slug || !existsSync(CLONES_DIR)) return null;
|
|
10
|
+
|
|
11
|
+
const entries = readdirSync(CLONES_DIR);
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
const fullPath = path.join(CLONES_DIR, entry);
|
|
14
|
+
try {
|
|
15
|
+
const stat = statSync(fullPath);
|
|
16
|
+
if (!stat.isDirectory()) continue;
|
|
17
|
+
if (!existsSync(path.join(fullPath, '.git'))) continue;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const git = simpleGit(fullPath);
|
|
21
|
+
const remotes = await git.getRemotes(true);
|
|
22
|
+
const origin = remotes.find((r) => r.name === 'origin') || remotes[0];
|
|
23
|
+
const info = extractCanonicalForgeSlugInfo(origin?.refs?.fetch || origin?.refs?.push || null);
|
|
24
|
+
if (info.slug === slug) {
|
|
25
|
+
return fullPath.replace(/\\/g, '/');
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// ignore invalid repos
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// ignore bad entries
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function POST(req: Request) {
|
|
39
|
+
try {
|
|
40
|
+
const { slug } = await req.json() as { slug?: string };
|
|
41
|
+
const normalizedSlug = (slug || '').trim().replace(/^\/+|\/+$/g, '');
|
|
42
|
+
if (!normalizedSlug) {
|
|
43
|
+
return Response.json({ error: 'slug is required' }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const resolvedPath = await findRepoByCanonicalSlug(normalizedSlug);
|
|
47
|
+
return Response.json({ path: resolvedPath });
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
return Response.json({ error: error?.message || 'Failed to resolve slug' }, { status: 500 });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,104 +1,188 @@
|
|
|
1
|
-
import { measure } from 'measure-fn';
|
|
2
|
-
import simpleGit from 'simple-git';
|
|
3
|
-
import { readFileSync, existsSync } from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { validateRepoPath } from '../validate-path';
|
|
6
|
-
|
|
7
|
-
const BINARY_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'svg', 'webp', 'mp3', 'mp4', 'wav', 'ogg', 'avi', 'mov', 'zip', 'tar', 'gz', 'rar', '7z', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'exe', 'dll', 'so', 'dylib', 'woff', 'woff2', 'ttf', 'eot', 'otf', 'lock']);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
1
|
+
import { measure } from 'measure-fn';
|
|
2
|
+
import simpleGit from 'simple-git';
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { validateRepoPath } from '../validate-path';
|
|
6
|
+
|
|
7
|
+
const BINARY_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'svg', 'webp', 'mp3', 'mp4', 'wav', 'ogg', 'avi', 'mov', 'zip', 'tar', 'gz', 'rar', '7z', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'exe', 'dll', 'so', 'dylib', 'woff', 'woff2', 'ttf', 'eot', 'otf', 'lock']);
|
|
8
|
+
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'svg', 'webp']);
|
|
9
|
+
const PDF_EXTS = new Set(['pdf']);
|
|
10
|
+
const MAX_READ_SIZE = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
export async function POST(req: Request) {
|
|
13
|
+
return measure('api:repo:tree', async () => {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const repoPath = body.path;
|
|
17
|
+
const stream = body.stream === true;
|
|
18
|
+
const includeAll = body.includeAll === true;
|
|
19
|
+
|
|
20
|
+
if (!repoPath) {
|
|
21
|
+
return new Response('Repository path is required', { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const blocked = validateRepoPath(repoPath);
|
|
25
|
+
if (blocked) return blocked;
|
|
26
|
+
|
|
27
|
+
const ignoreDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', 'coverage', '.turbo', '__pycache__', '.tsbuildinfo']);
|
|
28
|
+
|
|
29
|
+
// Recursively scan filesystem for all files (ignoring standard dirs)
|
|
30
|
+
function scanDir(dir: string, prefix: string): string[] {
|
|
31
|
+
const results: string[] = [];
|
|
32
|
+
try {
|
|
33
|
+
const entries = readdirSync(dir);
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (ignoreDirs.has(entry)) continue;
|
|
36
|
+
const fullPath = path.join(dir, entry);
|
|
37
|
+
const relativePath = prefix ? `${prefix}/${entry}` : entry;
|
|
38
|
+
try {
|
|
39
|
+
const stats = statSync(fullPath);
|
|
40
|
+
if (stats.isDirectory()) {
|
|
41
|
+
results.push(...scanDir(fullPath, relativePath));
|
|
42
|
+
} else if (stats.isFile()) {
|
|
43
|
+
results.push(relativePath);
|
|
44
|
+
}
|
|
45
|
+
} catch (e: any) {
|
|
46
|
+
console.warn(`[tree:scanDir] stat error: ${fullPath}: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (e: any) {
|
|
50
|
+
console.warn(`[tree:scanDir] readdir error: ${dir}: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let filePaths: string[];
|
|
56
|
+
|
|
57
|
+
const git = simpleGit(repoPath);
|
|
58
|
+
const isRepo = await git.checkIsRepo().catch(() => false);
|
|
59
|
+
|
|
60
|
+
if (!isRepo || includeAll) {
|
|
61
|
+
// Not a git repo or explicit all-files mode: scan filesystem
|
|
62
|
+
filePaths = scanDir(repoPath, '');
|
|
63
|
+
} else {
|
|
64
|
+
// Get tracked files
|
|
65
|
+
const result = await git.raw(['ls-files']);
|
|
66
|
+
const trackedPaths = result.trim().split('\n').filter(fp => {
|
|
67
|
+
if (!fp) return false;
|
|
68
|
+
if (Array.from(ignoreDirs).some(d => fp.startsWith(d + '/') || fp.startsWith(d + '\\'))) return false;
|
|
69
|
+
return true;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// If very few tracked files, also scan filesystem for untracked content
|
|
73
|
+
// This catches repos where most content (PDFs, images) is gitignored
|
|
74
|
+
if (trackedPaths.length < 50) {
|
|
75
|
+
const allPaths = scanDir(repoPath, '');
|
|
76
|
+
console.log(`[tree] ${trackedPaths.length} tracked, ${allPaths.length} on disk`);
|
|
77
|
+
if (allPaths.length > trackedPaths.length * 5) {
|
|
78
|
+
// Lots of untracked content — include everything
|
|
79
|
+
const trackedSet = new Set(trackedPaths);
|
|
80
|
+
filePaths = allPaths;
|
|
81
|
+
// But still filter out obvious junk from non-tracked scan
|
|
82
|
+
filePaths = filePaths.filter(fp => {
|
|
83
|
+
if (Array.from(ignoreDirs).some(d => fp.startsWith(d + '/') || fp.startsWith(d + '\\'))) return false;
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
console.log(`[tree] ${trackedPaths.length} tracked, ${allPaths.length} on disk → including all ${filePaths.length} files`);
|
|
87
|
+
} else {
|
|
88
|
+
filePaths = trackedPaths;
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
filePaths = trackedPaths;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readFile(filePath: string) {
|
|
96
|
+
const parts = filePath.split('/');
|
|
97
|
+
const name = parts[parts.length - 1];
|
|
98
|
+
const ext = name.includes('.') ? name.split('.').pop()!.toLowerCase() : '';
|
|
99
|
+
|
|
100
|
+
let content = null;
|
|
101
|
+
let lines = 0;
|
|
102
|
+
let size = 0;
|
|
103
|
+
let isBinary = BINARY_EXTS.has(ext);
|
|
104
|
+
const isImage = IMAGE_EXTS.has(ext);
|
|
105
|
+
const isPdf = PDF_EXTS.has(ext);
|
|
106
|
+
|
|
107
|
+
if (!isBinary) {
|
|
108
|
+
try {
|
|
109
|
+
const fullPath = path.join(repoPath, filePath);
|
|
110
|
+
const file = Bun.file(fullPath);
|
|
111
|
+
size = file.size;
|
|
112
|
+
|
|
113
|
+
// Skip reading content for very large files
|
|
114
|
+
if (size > MAX_READ_SIZE) {
|
|
115
|
+
isBinary = true;
|
|
116
|
+
} else {
|
|
117
|
+
const raw = readFileSync(fullPath, 'utf-8');
|
|
118
|
+
size = raw.length;
|
|
119
|
+
const allLines = raw.split('\n');
|
|
120
|
+
lines = allLines.length;
|
|
121
|
+
if (allLines.length > 10000) {
|
|
122
|
+
content = allLines.slice(0, 10000).join('\n');
|
|
123
|
+
} else {
|
|
124
|
+
content = raw;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
content = null;
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// For binary files, at least get the file size
|
|
132
|
+
try {
|
|
133
|
+
const fullPath = path.join(repoPath, filePath);
|
|
134
|
+
size = Bun.file(fullPath).size;
|
|
135
|
+
} catch (_) {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { path: filePath, name, ext, type: 'file', content, lines, size, isBinary, isImage, isPdf };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Streaming mode: NDJSON with total header ──
|
|
142
|
+
if (stream) {
|
|
143
|
+
const total = filePaths.length;
|
|
144
|
+
const BATCH_SIZE = 20;
|
|
145
|
+
const encoder = new TextEncoder();
|
|
146
|
+
|
|
147
|
+
const readable = new ReadableStream({
|
|
148
|
+
start(controller) {
|
|
149
|
+
// First line: total count
|
|
150
|
+
controller.enqueue(encoder.encode(JSON.stringify({ total }) + '\n'));
|
|
151
|
+
|
|
152
|
+
let i = 0;
|
|
153
|
+
function nextBatch() {
|
|
154
|
+
const end = Math.min(i + BATCH_SIZE, total);
|
|
155
|
+
const batch: any[] = [];
|
|
156
|
+
for (; i < end; i++) {
|
|
157
|
+
batch.push(readFile(filePaths[i]));
|
|
158
|
+
}
|
|
159
|
+
controller.enqueue(encoder.encode(JSON.stringify({ files: batch, loaded: i }) + '\n'));
|
|
160
|
+
|
|
161
|
+
if (i < total) {
|
|
162
|
+
// Yield to event loop between batches
|
|
163
|
+
setTimeout(nextBatch, 0);
|
|
164
|
+
} else {
|
|
165
|
+
controller.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
nextBatch();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return new Response(readable, {
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/x-ndjson',
|
|
175
|
+
'Cache-Control': 'no-cache',
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Legacy non-streaming mode ──
|
|
181
|
+
const files = filePaths.map(readFile);
|
|
182
|
+
return Response.json({ files, total: files.length });
|
|
183
|
+
} catch (error: any) {
|
|
184
|
+
console.error('api:repo:tree:error', error);
|
|
185
|
+
return new Response(`Error: ${error.message}`, { status: 500 });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
42
|
-
await
|
|
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
|
|
46
|
-
await
|
|
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,26 @@
|
|
|
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) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return proc.stdout.toString().trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function GET() {
|
|
19
|
+
const commit = process.env.GIT_COMMIT_HASH || runGit(['rev-parse', '--short', 'HEAD']) || 'unknown';
|
|
20
|
+
const commitDate = process.env.GIT_COMMIT_DATE || runGit(['log', '-1', '--format=%cs']) || '';
|
|
21
|
+
|
|
22
|
+
return Response.json({
|
|
23
|
+
commit,
|
|
24
|
+
commitDate,
|
|
25
|
+
});
|
|
26
|
+
}
|