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.
Files changed (145) hide show
  1. package/README.md +265 -122
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/[owner]/[repo]/page.client.tsx +5 -0
  5. package/app/[slug]/page.client.tsx +5 -0
  6. package/app/analytics.db +0 -0
  7. package/app/api/analytics/route.ts +64 -0
  8. package/app/api/auth/positions/route.ts +95 -33
  9. package/app/api/build-info/route.ts +19 -0
  10. package/app/api/chat/route.ts +13 -2
  11. package/app/api/manifest.json/route.ts +20 -0
  12. package/app/api/og-image/route.ts +14 -0
  13. package/app/api/pwa-icon/route.ts +14 -0
  14. package/app/api/repo/clone-stream/route.ts +20 -12
  15. package/app/api/repo/file-content/route.ts +73 -20
  16. package/app/api/repo/imports/route.ts +21 -3
  17. package/app/api/repo/list/route.ts +30 -0
  18. package/app/api/repo/load/route.test.ts +62 -0
  19. package/app/api/repo/load/route.ts +41 -1
  20. package/app/api/repo/pdf-thumb/route.ts +127 -0
  21. package/app/api/repo/resolve-slug/route.ts +51 -0
  22. package/app/api/repo/tree/route.ts +188 -104
  23. package/app/api/repo/upload/route.ts +6 -9
  24. package/app/api/sw.js/route.ts +70 -0
  25. package/app/api/version/route.ts +26 -0
  26. package/app/galaxy-canvas/page.client.tsx +2 -0
  27. package/app/galaxy-canvas/page.tsx +5 -0
  28. package/app/globals.css +5844 -4694
  29. package/app/icon.png +0 -0
  30. package/app/layout.tsx +1284 -467
  31. package/app/lib/auto-arrange.test.ts +158 -0
  32. package/app/lib/auto-arrange.ts +147 -0
  33. package/app/lib/canvas-export.ts +358 -358
  34. package/app/lib/canvas-text.ts +4 -72
  35. package/app/lib/canvas.ts +625 -564
  36. package/app/lib/card-arrangement.ts +21 -7
  37. package/app/lib/card-context-menu.tsx +2 -2
  38. package/app/lib/card-groups.ts +9 -2
  39. package/app/lib/cards.tsx +1361 -914
  40. package/app/lib/chat.tsx +65 -9
  41. package/app/lib/code-editor.ts +86 -2
  42. package/app/lib/connections.tsx +34 -43
  43. package/app/lib/context.test.ts +32 -0
  44. package/app/lib/context.ts +19 -3
  45. package/app/lib/cursor-sharing.ts +34 -0
  46. package/app/lib/events.tsx +76 -73
  47. package/app/lib/export-canvas.ts +287 -0
  48. package/app/lib/file-card-plugin.ts +148 -134
  49. package/app/lib/file-modal.tsx +49 -0
  50. package/app/lib/file-preview.ts +486 -400
  51. package/app/lib/github-import.test.ts +424 -0
  52. package/app/lib/global-search.ts +48 -27
  53. package/app/lib/initial-route-hydration.test.ts +283 -0
  54. package/app/lib/initial-route-hydration.ts +202 -0
  55. package/app/lib/landing-reset.test.ts +99 -0
  56. package/app/lib/landing-reset.ts +106 -0
  57. package/app/lib/landing-shell.test.ts +75 -0
  58. package/app/lib/large-repo-optimization.ts +37 -0
  59. package/app/lib/layers.tsx +17 -18
  60. package/app/lib/layout-snapshots.ts +320 -0
  61. package/app/lib/loading.test.ts +69 -0
  62. package/app/lib/loading.tsx +160 -45
  63. package/app/lib/mount-cleanup.test.ts +52 -0
  64. package/app/lib/mount-cleanup.ts +34 -0
  65. package/app/lib/mount-init.test.ts +123 -0
  66. package/app/lib/mount-init.ts +107 -0
  67. package/app/lib/mount-lifecycle.test.ts +39 -0
  68. package/app/lib/mount-lifecycle.ts +12 -0
  69. package/app/lib/mount-route-wiring.test.ts +87 -0
  70. package/app/lib/mount-route-wiring.ts +84 -0
  71. package/app/lib/multi-repo.ts +14 -0
  72. package/app/lib/onboarding-tutorial.ts +278 -0
  73. package/app/lib/perf-overlay.ts +78 -0
  74. package/app/lib/positions.ts +191 -122
  75. package/app/lib/recent-commits.test.ts +869 -0
  76. package/app/lib/recent-commits.ts +227 -0
  77. package/app/lib/repo-handoff.test.ts +23 -0
  78. package/app/lib/repo-handoff.ts +16 -0
  79. package/app/lib/repo-progressive.ts +119 -0
  80. package/app/lib/repo-select.test.ts +61 -0
  81. package/app/lib/repo-select.ts +74 -0
  82. package/app/lib/repo.tsx +1383 -977
  83. package/app/lib/role.ts +228 -0
  84. package/app/lib/route-catchall.test.ts +27 -0
  85. package/app/lib/route-repo-entry.test.ts +95 -0
  86. package/app/lib/route-repo-entry.ts +36 -0
  87. package/app/lib/router-contract.test.ts +22 -0
  88. package/app/lib/router-contract.ts +19 -0
  89. package/app/lib/shared-layout.test.ts +86 -0
  90. package/app/lib/shared-layout.ts +82 -0
  91. package/app/lib/shortcuts-panel.ts +2 -0
  92. package/app/lib/status-bar.test.ts +118 -0
  93. package/app/lib/status-bar.ts +365 -128
  94. package/app/lib/sync-controls.test.ts +43 -0
  95. package/app/lib/sync-controls.tsx +303 -0
  96. package/app/lib/test-dom.ts +145 -0
  97. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  98. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  99. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  100. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  101. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  102. package/app/lib/transclusion-smoke.test.ts +163 -0
  103. package/app/lib/tutorial.ts +301 -0
  104. package/app/lib/version.ts +93 -0
  105. package/app/lib/viewport-culling.ts +740 -728
  106. package/app/lib/virtual-files.ts +456 -0
  107. package/app/lib/webgl-text.ts +189 -0
  108. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
  109. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  110. package/app/og-image.png +0 -0
  111. package/app/page.client.tsx +70 -215
  112. package/app/page.tsx +27 -92
  113. package/app/state/machine.js +13 -0
  114. package/banner.png +0 -0
  115. package/package.json +17 -8
  116. package/server.ts +11 -1
  117. package/app/api/connections/route.ts +0 -72
  118. package/app/api/positions/route.ts +0 -80
  119. package/app/api/repo/browse/route.ts +0 -55
  120. package/app/lib/pr-review.ts +0 -374
  121. package/packages/galaxydraw/README.md +0 -296
  122. package/packages/galaxydraw/banner.png +0 -0
  123. package/packages/galaxydraw/demo/build-static.ts +0 -100
  124. package/packages/galaxydraw/demo/client.ts +0 -154
  125. package/packages/galaxydraw/demo/dist/client.js +0 -8
  126. package/packages/galaxydraw/demo/index.html +0 -256
  127. package/packages/galaxydraw/demo/server.ts +0 -96
  128. package/packages/galaxydraw/dist/index.js +0 -984
  129. package/packages/galaxydraw/dist/index.js.map +0 -16
  130. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  131. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  132. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  133. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  134. package/packages/galaxydraw/package.json +0 -49
  135. package/packages/galaxydraw/perf.test.ts +0 -284
  136. package/packages/galaxydraw/src/core/cards.ts +0 -435
  137. package/packages/galaxydraw/src/core/engine.ts +0 -339
  138. package/packages/galaxydraw/src/core/events.ts +0 -81
  139. package/packages/galaxydraw/src/core/layout.ts +0 -136
  140. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  141. package/packages/galaxydraw/src/core/state.ts +0 -177
  142. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  143. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  144. package/packages/galaxydraw/src/index.ts +0 -40
  145. 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
- export async function POST(req: Request) {
10
- return measure('api:repo:tree', async () => {
11
- try {
12
- const { path: repoPath } = await req.json();
13
-
14
- if (!repoPath) {
15
- return new Response('Repository path is required', { status: 400 });
16
- }
17
-
18
- const blocked = validateRepoPath(repoPath);
19
- if (blocked) return blocked;
20
-
21
- const git = simpleGit(repoPath);
22
-
23
- // Get tracked files only (respects .gitignore by definition)
24
- const result = await git.raw(['ls-files']);
25
- const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '.next', '.cache', 'coverage', '.turbo', '__pycache__', '.tsbuildinfo'];
26
-
27
- // Also parse .gitignore for extra patterns
28
- const gitignorePatterns: string[] = [];
29
- const gitignorePath = path.join(repoPath, '.gitignore');
30
- if (existsSync(gitignorePath)) {
31
- try {
32
- const content = readFileSync(gitignorePath, 'utf-8');
33
- content.split('\n').forEach(line => {
34
- line = line.trim();
35
- if (line && !line.startsWith('#')) {
36
- // Normalize: remove trailing slashes for dir matching
37
- const clean = line.replace(/\/+$/, '');
38
- if (clean) gitignorePatterns.push(clean);
39
- }
40
- });
41
- } catch (e) { /* ignore */ }
42
- }
43
-
44
- const filePaths = result.trim().split('\n').filter(fp => {
45
- if (!fp) return false;
46
- // Filter out known heavy directories
47
- if (ignoreDirs.some(d => fp.startsWith(d + '/') || fp.startsWith(d + '\\'))) return false;
48
- // Filter out files matching gitignore patterns (extra safety)
49
- for (const pattern of gitignorePatterns) {
50
- if (fp.startsWith(pattern + '/') || fp.startsWith(pattern + '\\')) return false;
51
- if (fp === pattern) return false;
52
- // Simple glob: *.ext
53
- if (pattern.startsWith('*.')) {
54
- const ext = pattern.substring(1); // .ext
55
- if (fp.endsWith(ext)) return false;
56
- }
57
- }
58
- return true;
59
- });
60
-
61
- const files = filePaths.map(filePath => {
62
- const parts = filePath.split('/');
63
- const name = parts[parts.length - 1];
64
- const ext = name.includes('.') ? name.split('.').pop()!.toLowerCase() : '';
65
-
66
- let content = null;
67
- let lines = 0;
68
- let isBinary = BINARY_EXTS.has(ext);
69
-
70
- if (!isBinary) {
71
- try {
72
- const fullPath = path.join(repoPath, filePath);
73
- const raw = readFileSync(fullPath, 'utf-8');
74
- const allLines = raw.split('\n');
75
- lines = allLines.length;
76
- // Send full content for small/medium files, truncate very large ones
77
- if (allLines.length > 10000) {
78
- content = allLines.slice(0, 10000).join('\n');
79
- } else {
80
- content = raw;
81
- }
82
- } catch (e) {
83
- content = null;
84
- }
85
- }
86
-
87
- return {
88
- path: filePath,
89
- name,
90
- ext,
91
- type: 'file',
92
- content,
93
- lines,
94
- isBinary
95
- };
96
- });
97
-
98
- return Response.json({ files, total: files.length });
99
- } catch (error: any) {
100
- console.error('api:repo:tree:error', error);
101
- return new Response(`Error: ${error.message}`, { status: 500 });
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 { 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,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
+ }
@@ -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;