gitmaps 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +167 -0
  2. package/app/api/auth/favorites/route.ts +56 -0
  3. package/app/api/auth/github/callback/route.ts +103 -0
  4. package/app/api/auth/github/route.ts +32 -0
  5. package/app/api/auth/me/route.ts +52 -0
  6. package/app/api/auth/positions/route.ts +50 -0
  7. package/app/api/chat/route.ts +101 -0
  8. package/app/api/connections/route.ts +72 -0
  9. package/app/api/github/repos/route.ts +111 -0
  10. package/app/api/positions/route.ts +80 -0
  11. package/app/api/repo/branch-diff/route.ts +201 -0
  12. package/app/api/repo/branches/route.ts +53 -0
  13. package/app/api/repo/browse/route.ts +55 -0
  14. package/app/api/repo/clone/route.ts +78 -0
  15. package/app/api/repo/clone-stream/route.ts +131 -0
  16. package/app/api/repo/file-content/route.ts +28 -0
  17. package/app/api/repo/file-delete/route.ts +62 -0
  18. package/app/api/repo/file-history/route.ts +45 -0
  19. package/app/api/repo/file-rename/route.ts +83 -0
  20. package/app/api/repo/file-save/route.ts +45 -0
  21. package/app/api/repo/files/route.ts +169 -0
  22. package/app/api/repo/git-blame/route.ts +86 -0
  23. package/app/api/repo/git-commit/route.ts +40 -0
  24. package/app/api/repo/git-heatmap/route.ts +55 -0
  25. package/app/api/repo/imports/route.ts +154 -0
  26. package/app/api/repo/load/route.ts +56 -0
  27. package/app/api/repo/mode/route.ts +14 -0
  28. package/app/api/repo/search/route.ts +127 -0
  29. package/app/api/repo/tree/route.ts +104 -0
  30. package/app/api/repo/upload/route.ts +53 -0
  31. package/app/api/repo/validate-path.ts +53 -0
  32. package/app/canvas_users.db +0 -0
  33. package/app/canvas_users.db-shm +0 -0
  34. package/app/canvas_users.db-wal +0 -0
  35. package/app/globals.css +7899 -0
  36. package/app/layout.tsx +493 -0
  37. package/app/lib/auth.ts +193 -0
  38. package/app/lib/auto-save.ts +137 -0
  39. package/app/lib/branch-compare.ts +443 -0
  40. package/app/lib/breadcrumbs.ts +170 -0
  41. package/app/lib/canvas-export.ts +358 -0
  42. package/app/lib/canvas-text.ts +912 -0
  43. package/app/lib/canvas.ts +564 -0
  44. package/app/lib/card-arrangement.ts +188 -0
  45. package/app/lib/card-context-menu.tsx +453 -0
  46. package/app/lib/card-diff-markers.ts +270 -0
  47. package/app/lib/card-expand.ts +189 -0
  48. package/app/lib/card-groups.ts +246 -0
  49. package/app/lib/cards.tsx +914 -0
  50. package/app/lib/chat.tsx +308 -0
  51. package/app/lib/code-editor.ts +508 -0
  52. package/app/lib/command-palette.ts +262 -0
  53. package/app/lib/connections.tsx +1037 -0
  54. package/app/lib/context.ts +94 -0
  55. package/app/lib/cursor-sharing.ts +281 -0
  56. package/app/lib/dependency-graph.ts +438 -0
  57. package/app/lib/events.tsx +1747 -0
  58. package/app/lib/file-card-plugin.ts +134 -0
  59. package/app/lib/file-modal.tsx +849 -0
  60. package/app/lib/file-preview.ts +400 -0
  61. package/app/lib/file-tabs.ts +318 -0
  62. package/app/lib/galaxydraw-bridge.ts +477 -0
  63. package/app/lib/galaxydraw.test.ts +229 -0
  64. package/app/lib/global-search.ts +264 -0
  65. package/app/lib/goto-definition.ts +224 -0
  66. package/app/lib/heatmap.ts +178 -0
  67. package/app/lib/hidden-files.tsx +222 -0
  68. package/app/lib/layers.ts +0 -0
  69. package/app/lib/layers.tsx +365 -0
  70. package/app/lib/loading.tsx +45 -0
  71. package/app/lib/multi-repo.ts +286 -0
  72. package/app/lib/new-file-dialog.tsx +230 -0
  73. package/app/lib/onboarding.tsx +213 -0
  74. package/app/lib/perf-overlay.ts +360 -0
  75. package/app/lib/positions.ts +176 -0
  76. package/app/lib/pr-review.ts +374 -0
  77. package/app/lib/production-mode.ts +47 -0
  78. package/app/lib/repo.tsx +977 -0
  79. package/app/lib/settings-modal.tsx +374 -0
  80. package/app/lib/settings.ts +97 -0
  81. package/app/lib/shortcuts-panel.ts +141 -0
  82. package/app/lib/status-bar.ts +128 -0
  83. package/app/lib/symbol-outline.ts +212 -0
  84. package/app/lib/syntax.ts +177 -0
  85. package/app/lib/tab-diff.ts +238 -0
  86. package/app/lib/user.tsx +133 -0
  87. package/app/lib/utils.ts +78 -0
  88. package/app/lib/viewport-culling.ts +728 -0
  89. package/app/page.client.tsx +215 -0
  90. package/app/page.tsx +291 -0
  91. package/app/state/machine.js +196 -0
  92. package/app/styles/main.css +2168 -0
  93. package/banner.png +0 -0
  94. package/cli.ts +44 -0
  95. package/package.json +75 -0
  96. package/packages/galaxydraw/README.md +296 -0
  97. package/packages/galaxydraw/banner.png +0 -0
  98. package/packages/galaxydraw/demo/build-static.ts +100 -0
  99. package/packages/galaxydraw/demo/client.ts +154 -0
  100. package/packages/galaxydraw/demo/dist/client.js +8 -0
  101. package/packages/galaxydraw/demo/index.html +256 -0
  102. package/packages/galaxydraw/demo/server.ts +96 -0
  103. package/packages/galaxydraw/dist/index.js +984 -0
  104. package/packages/galaxydraw/dist/index.js.map +16 -0
  105. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  106. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  109. package/packages/galaxydraw/package.json +49 -0
  110. package/packages/galaxydraw/perf.test.ts +284 -0
  111. package/packages/galaxydraw/src/core/cards.ts +435 -0
  112. package/packages/galaxydraw/src/core/engine.ts +339 -0
  113. package/packages/galaxydraw/src/core/events.ts +81 -0
  114. package/packages/galaxydraw/src/core/layout.ts +136 -0
  115. package/packages/galaxydraw/src/core/minimap.ts +216 -0
  116. package/packages/galaxydraw/src/core/state.ts +177 -0
  117. package/packages/galaxydraw/src/core/viewport.ts +106 -0
  118. package/packages/galaxydraw/src/galaxydraw.css +166 -0
  119. package/packages/galaxydraw/src/index.ts +40 -0
  120. package/packages/galaxydraw/tsconfig.json +30 -0
  121. package/server.ts +62 -0
@@ -0,0 +1,40 @@
1
+ import { measure } from 'measure-fn';
2
+ import simpleGit from 'simple-git';
3
+ import { validateRepoPath } from '../validate-path';
4
+
5
+ export async function POST(req: Request) {
6
+ return measure('api:repo:git-commit', async () => {
7
+ try {
8
+ const { path: repoPath, filePath, message } = await req.json();
9
+
10
+ if (!repoPath || !filePath || !message) {
11
+ return new Response('Repository path, file path, and commit message are required', { status: 400 });
12
+ }
13
+
14
+ const blocked = validateRepoPath(repoPath);
15
+ if (blocked) return blocked;
16
+
17
+ const git = simpleGit(repoPath);
18
+
19
+ // Stage the specific file
20
+ await git.add(filePath);
21
+
22
+ // Get the staged diff for context
23
+ const diffSummary = await git.diffSummary(['--cached']);
24
+
25
+ // Commit
26
+ const result = await git.commit(message, filePath);
27
+
28
+ return Response.json({
29
+ success: true,
30
+ hash: result.commit || '',
31
+ summary: result.summary || {},
32
+ branch: result.branch || '',
33
+ filesChanged: diffSummary.files?.length || 1,
34
+ });
35
+ } catch (error: any) {
36
+ console.error('api:repo:git-commit:error', error);
37
+ return new Response(`Error: ${error.message}`, { status: 500 });
38
+ }
39
+ });
40
+ }
@@ -0,0 +1,55 @@
1
+ import { measure } from 'measure-fn';
2
+ import simpleGit from 'simple-git';
3
+ import { validateRepoPath } from '../validate-path';
4
+
5
+ /**
6
+ * Git Heatmap API — returns commit frequency per file for the given time range.
7
+ * Used by the canvas heatmap overlay to color-code files by activity.
8
+ */
9
+ export async function POST(req: Request) {
10
+ return measure('api:repo:git-heatmap', async () => {
11
+ try {
12
+ const { path: repoPath, days = 90 } = 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
+ const since = `${days} days ago`;
23
+
24
+ // Get all file changes in the time range: one filename per line
25
+ const raw = await git.raw([
26
+ 'log', '--format=format:', '--name-only', `--since=${since}`
27
+ ]);
28
+
29
+ // Count occurrences of each file
30
+ const counts: Record<string, number> = {};
31
+ let maxCount = 0;
32
+
33
+ for (const line of raw.split('\n')) {
34
+ const file = line.trim();
35
+ if (!file) continue;
36
+ counts[file] = (counts[file] || 0) + 1;
37
+ if (counts[file]! > maxCount) maxCount = counts[file]!;
38
+ }
39
+
40
+ // Build sorted array with normalized heat (0-1)
41
+ const files = Object.entries(counts)
42
+ .map(([file, count]) => ({
43
+ file,
44
+ commits: count,
45
+ heat: maxCount > 0 ? count / maxCount : 0,
46
+ }))
47
+ .sort((a, b) => b.commits - a.commits);
48
+
49
+ return Response.json({ files, maxCommits: maxCount, days, totalFiles: files.length });
50
+ } catch (error: any) {
51
+ console.error('api:repo:git-heatmap:error', error);
52
+ return new Response(`Error: ${error.message}`, { status: 500 });
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,154 @@
1
+ import { measure } from 'measure-fn';
2
+ import simpleGit from 'simple-git';
3
+ import { validateRepoPath } from '../validate-path';
4
+
5
+ /**
6
+ * POST /api/repo/imports
7
+ * Body: { path: string, commit: string }
8
+ *
9
+ * Scans all source files at the given commit and returns import/require
10
+ * relationships as edges: { source: string, target: string, line: number }[]
11
+ *
12
+ * Supports: ES import, CommonJS require, CSS @import, Python import
13
+ */
14
+
15
+ const SOURCE_EXTENSIONS = new Set([
16
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
17
+ '.vue', '.svelte',
18
+ '.css', '.scss', '.less',
19
+ '.py',
20
+ ]);
21
+
22
+ // Match import/require patterns and extract the module specifier
23
+ const IMPORT_PATTERNS = [
24
+ // ES: import ... from 'module' or import 'module'
25
+ /(?:import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"])/g,
26
+ // ES: export ... from 'module'
27
+ /(?:export\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"])/g,
28
+ // CommonJS: require('module')
29
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
30
+ // CSS: @import 'file' or @import url('file')
31
+ /@import\s+(?:url\s*\(\s*)?['"]([^'"]+)['"]/g,
32
+ // Python: from module import ... or import module
33
+ /(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))/g,
34
+ ];
35
+
36
+ function resolveImport(sourceFile: string, specifier: string, allFiles: string[]): string | null {
37
+ // Skip node_modules / external packages
38
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) return null;
39
+
40
+ // Get directory of source file
41
+ const sourceDir = sourceFile.includes('/') ? sourceFile.substring(0, sourceFile.lastIndexOf('/')) : '';
42
+
43
+ // Resolve relative path
44
+ let resolved: string;
45
+ if (specifier.startsWith('/')) {
46
+ resolved = specifier.substring(1);
47
+ } else {
48
+ const parts = sourceDir.split('/').filter(Boolean);
49
+ const specParts = specifier.split('/');
50
+ for (const sp of specParts) {
51
+ if (sp === '..') parts.pop();
52
+ else if (sp !== '.') parts.push(sp);
53
+ }
54
+ resolved = parts.join('/');
55
+ }
56
+
57
+ // Try exact match first
58
+ if (allFiles.includes(resolved)) return resolved;
59
+
60
+ // Try adding extensions
61
+ const tryExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.css', '.scss', '.vue', '.svelte'];
62
+ for (const ext of tryExts) {
63
+ if (allFiles.includes(resolved + ext)) return resolved + ext;
64
+ }
65
+
66
+ // Try /index
67
+ for (const ext of tryExts) {
68
+ if (allFiles.includes(resolved + '/index' + ext)) return resolved + '/index' + ext;
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ export async function POST(req: Request) {
75
+ return measure('api:repo:imports', async () => {
76
+ try {
77
+ const { path: repoPath, commit } = await req.json();
78
+
79
+ if (!repoPath || !commit) {
80
+ return Response.json({ error: 'path and commit required' }, { status: 400 });
81
+ }
82
+
83
+ const blocked = validateRepoPath(repoPath);
84
+ if (blocked) return blocked;
85
+
86
+ const git = simpleGit(repoPath);
87
+
88
+ // Get all files at this commit
89
+ const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', commit]);
90
+ const allFiles = lsOutput.split('\n').filter(Boolean);
91
+
92
+ // Filter to source files
93
+ const sourceFiles = allFiles.filter(f => {
94
+ const ext = '.' + f.split('.').pop()?.toLowerCase();
95
+ return SOURCE_EXTENSIONS.has(ext);
96
+ });
97
+
98
+ // Scan each source file for imports (limit to first 200 files for perf)
99
+ const filesToScan = sourceFiles.slice(0, 200);
100
+ const edges: { source: string; target: string; line: number }[] = [];
101
+
102
+ await Promise.allSettled(filesToScan.map(async (filePath) => {
103
+ try {
104
+ const content = await git.show([`${commit}:${filePath}`]);
105
+ const lines = content.split('\n');
106
+
107
+ for (let i = 0; i < Math.min(lines.length, 100); i++) {
108
+ // Only scan first 100 lines (imports are at the top)
109
+ const line = lines[i];
110
+
111
+ for (const pattern of IMPORT_PATTERNS) {
112
+ // Reset lastIndex for global regex
113
+ const regex = new RegExp(pattern.source, pattern.flags);
114
+ let match;
115
+ while ((match = regex.exec(line)) !== null) {
116
+ const specifier = match[1] || match[2];
117
+ if (!specifier) continue;
118
+
119
+ const resolved = resolveImport(filePath, specifier, allFiles);
120
+ if (resolved && resolved !== filePath) {
121
+ edges.push({
122
+ source: filePath,
123
+ target: resolved,
124
+ line: i + 1,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ }
130
+ } catch { /* file might be binary or unreadable */ }
131
+ }));
132
+
133
+ // Deduplicate edges
134
+ const seen = new Set<string>();
135
+ const unique = edges.filter(e => {
136
+ const key = `${e.source}→${e.target}`;
137
+ if (seen.has(key)) return false;
138
+ seen.add(key);
139
+ return true;
140
+ });
141
+
142
+ console.log(`[imports] ${filesToScan.length} files scanned, ${unique.length} import edges found`);
143
+
144
+ return Response.json({
145
+ edges: unique,
146
+ filesScanned: filesToScan.length,
147
+ totalFiles: allFiles.length,
148
+ });
149
+ } catch (error: any) {
150
+ console.error('api:repo:imports:error', error);
151
+ return Response.json({ error: error.message }, { status: 500 });
152
+ }
153
+ });
154
+ }
@@ -0,0 +1,56 @@
1
+ import { measure } from 'measure-fn';
2
+ import simpleGit from 'simple-git';
3
+ import path from 'path';
4
+ import { validateRepoPath } from '../validate-path';
5
+
6
+ export async function POST(req: Request) {
7
+ return measure('api:repo:load', async () => {
8
+ try {
9
+ const { path: repoPath } = await req.json();
10
+
11
+ if (!repoPath) {
12
+ return new Response('Repository path is required', { status: 400 });
13
+ }
14
+
15
+ const blocked = validateRepoPath(repoPath);
16
+ if (blocked) return blocked;
17
+
18
+ const git = simpleGit(repoPath);
19
+
20
+ // Check if it's a git repository
21
+ const isRepo = await git.checkIsRepo();
22
+ if (!isRepo) {
23
+ return new Response('Not a valid git repository', { status: 400 });
24
+ }
25
+
26
+ // Get commit log (last 100 commits) with custom format for tree graph
27
+ const log = await git.log({
28
+ maxCount: 100,
29
+ format: {
30
+ hash: '%H',
31
+ parents: '%P',
32
+ message: '%s',
33
+ author_name: '%an',
34
+ author_email: '%ae',
35
+ date: '%ai',
36
+ refs: '%D' // e.g. "HEAD -> main, origin/main, origin/HEAD"
37
+ }
38
+ });
39
+
40
+ const commits = log.all.map(commit => ({
41
+ hash: commit.hash,
42
+ parents: commit.parents ? commit.parents.trim().split(' ').filter(Boolean) : [],
43
+ message: commit.message.split('\n')[0], // First line only
44
+ author: commit.author_name,
45
+ email: commit.author_email,
46
+ date: commit.date,
47
+ refs: commit.refs ? commit.refs.split(',').map(r => r.trim()).filter(Boolean) : []
48
+ }));
49
+
50
+ return Response.json({ commits });
51
+ } catch (error: any) {
52
+ console.error('api:repo:load:error', error);
53
+ return new Response(`Error: ${error.message}`, { status: 500 });
54
+ }
55
+ });
56
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * GET /api/repo/mode
3
+ * Returns the app mode: 'local' (dev) or 'saas' (production).
4
+ * In local mode, users can browse local folders AND clone remote URLs.
5
+ * In saas mode, only remote URL cloning is available.
6
+ */
7
+ export async function GET() {
8
+ const env = process.env.NODE_ENV || 'development';
9
+ const isLocal = env === 'development' || env === 'local' || env === 'dev';
10
+ return Response.json({
11
+ mode: isLocal ? 'local' : 'saas',
12
+ env,
13
+ });
14
+ }
@@ -0,0 +1,127 @@
1
+ import { measure } from 'measure-fn';
2
+ import simpleGit from 'simple-git';
3
+ import { validateRepoPath } from '../validate-path';
4
+
5
+ /**
6
+ * POST /api/repo/search
7
+ * Body: { path, query, commit?, maxResults?, caseSensitive? }
8
+ * Uses `git grep` for fast full-text search across the repo.
9
+ * Returns: { results: [{ file, matches: [{ line, content, lineNumber }] }], totalMatches }
10
+ */
11
+ export async function POST(req: Request) {
12
+ return measure('api:repo:search', async () => {
13
+ try {
14
+ const { path: repoPath, query, commit, maxResults = 200, caseSensitive = false } = await req.json();
15
+
16
+ if (!repoPath || !query) {
17
+ return new Response('Repository path and query are required', { status: 400 });
18
+ }
19
+
20
+ if (query.length < 2) {
21
+ return new Response('Query must be at least 2 characters', { status: 400 });
22
+ }
23
+
24
+ const blocked = validateRepoPath(repoPath);
25
+ if (blocked) return blocked;
26
+
27
+ const git = simpleGit(repoPath);
28
+
29
+ // Build git grep args
30
+ const args = ['grep', '-n', '--break', '--heading'];
31
+ if (!caseSensitive) args.push('-i');
32
+ // Limit per-file matches to avoid overwhelming results
33
+ args.push('--max-count=20');
34
+
35
+ if (commit) {
36
+ args.push(commit, '--', query);
37
+ } else {
38
+ // Search working tree
39
+ args.push('--', query);
40
+ }
41
+
42
+ // Actually git grep uses the query as a positional arg, let me restructure
43
+ // git grep [-i] [-n] [--max-count=N] <pattern> [<commit>] [-- <pathspec>]
44
+ const grepArgs: string[] = ['-n'];
45
+ if (!caseSensitive) grepArgs.push('-i');
46
+ grepArgs.push('--max-count=20');
47
+ grepArgs.push('-e', query);
48
+
49
+ if (commit) {
50
+ grepArgs.push(commit);
51
+ }
52
+
53
+ let rawOutput: string;
54
+ try {
55
+ rawOutput = await git.raw(['grep', ...grepArgs]);
56
+ } catch (err: any) {
57
+ // git grep returns exit code 1 when no matches found
58
+ if (err.message?.includes('exit code 1') || err.message?.includes('process exited with code 1')) {
59
+ return Response.json({ results: [], totalMatches: 0 });
60
+ }
61
+ throw err;
62
+ }
63
+
64
+ if (!rawOutput?.trim()) {
65
+ return Response.json({ results: [], totalMatches: 0 });
66
+ }
67
+
68
+ // Parse git grep output: <file>:<lineNum>:<content>
69
+ // Or with commit: <commit>:<file>:<lineNum>:<content>
70
+ const fileGroups = new Map<string, { line: number; content: string }[]>();
71
+ let totalMatches = 0;
72
+
73
+ for (const line of rawOutput.split('\n')) {
74
+ if (!line.trim()) continue;
75
+
76
+ let filePath: string;
77
+ let lineNum: number;
78
+ let content: string;
79
+
80
+ if (commit) {
81
+ // Format: <commit>:<file>:<lineNum>:<content>
82
+ const commitPrefix = commit + ':';
83
+ if (!line.startsWith(commitPrefix)) continue;
84
+ const rest = line.slice(commitPrefix.length);
85
+ const firstColon = rest.indexOf(':');
86
+ if (firstColon < 0) continue;
87
+ const afterFile = rest.slice(firstColon + 1);
88
+ const secondColon = afterFile.indexOf(':');
89
+ if (secondColon < 0) continue;
90
+ filePath = rest.slice(0, firstColon);
91
+ lineNum = parseInt(afterFile.slice(0, secondColon), 10);
92
+ content = afterFile.slice(secondColon + 1);
93
+ } else {
94
+ // Format: <file>:<lineNum>:<content>
95
+ const firstColon = line.indexOf(':');
96
+ if (firstColon < 0) continue;
97
+ const afterFile = line.slice(firstColon + 1);
98
+ const secondColon = afterFile.indexOf(':');
99
+ if (secondColon < 0) continue;
100
+ filePath = line.slice(0, firstColon);
101
+ lineNum = parseInt(afterFile.slice(0, secondColon), 10);
102
+ content = afterFile.slice(secondColon + 1);
103
+ }
104
+
105
+ if (isNaN(lineNum)) continue;
106
+
107
+ if (!fileGroups.has(filePath)) {
108
+ fileGroups.set(filePath, []);
109
+ }
110
+ fileGroups.get(filePath)!.push({ line: lineNum, content: content.trimEnd() });
111
+ totalMatches++;
112
+
113
+ if (totalMatches >= maxResults) break;
114
+ }
115
+
116
+ const results = Array.from(fileGroups.entries()).map(([file, matches]) => ({
117
+ file,
118
+ matches,
119
+ }));
120
+
121
+ return Response.json({ results, totalMatches });
122
+ } catch (error: any) {
123
+ console.error('api:repo:search:error', error);
124
+ return new Response(`Error: ${error.message}`, { status: 500 });
125
+ }
126
+ });
127
+ }
@@ -0,0 +1,104 @@
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
+ }
@@ -0,0 +1,53 @@
1
+ import { mkdir, writeFile } from "fs/promises";
2
+ import * as path from "path";
3
+ import { exec } from "child_process";
4
+ import { promisify } from "util";
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ export async function POST(req: Request) {
9
+ try {
10
+ const formData = await req.formData();
11
+ const files = formData.getAll('files') as File[];
12
+
13
+ if (!files || files.length === 0) {
14
+ return Response.json({ error: 'No files provided' }, { status: 400 });
15
+ }
16
+
17
+ // Generate a unique ID for this upload
18
+ const uploadId = `upload_${Date.now()}_${Math.random().toString(36).substring(7)}`;
19
+ const repoPath = path.resolve(`.data/uploads/${uploadId}`);
20
+
21
+ // Ensure directories exist
22
+ await mkdir(repoPath, { recursive: true });
23
+
24
+ // Write all files
25
+ for (const file of files) {
26
+ const relativePath = file.name; // We passed the full path in formData.append('files', f, f.fullPath)
27
+ if (relativePath.includes('..') || relativePath.includes('\0')) {
28
+ continue; // Basic security prevention
29
+ }
30
+ const fullPath = path.join(repoPath, relativePath);
31
+ await mkdir(path.dirname(fullPath), { recursive: true });
32
+
33
+ const buffer = Buffer.from(await file.arrayBuffer());
34
+ await writeFile(fullPath, buffer);
35
+ }
36
+
37
+ // Initialize a Git repository so galaxy-canvas can read it
38
+ await execAsync(`git init`, { cwd: repoPath });
39
+
40
+ // 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 });
43
+
44
+ // 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 });
47
+
48
+ return Response.json({ path: repoPath, success: true });
49
+ } catch (e: any) {
50
+ console.error("Upload error:", e);
51
+ return Response.json({ error: e.message || 'Failed to upload files' }, { status: 500 });
52
+ }
53
+ }
@@ -0,0 +1,53 @@
1
+ import path from 'path';
2
+
3
+ /**
4
+ * Allowed repo path directories in production (SaaS) mode.
5
+ * Only these directories can be accessed — prevents arbitrary file system reads.
6
+ */
7
+ const ALLOWED_ROOTS = [
8
+ path.resolve(process.cwd(), 'git-canvas', 'repos'), // Clone directory
9
+ path.resolve(process.cwd(), '.data', 'uploads'), // Drag-and-drop uploads
10
+ ];
11
+
12
+ const IS_PRODUCTION = (() => {
13
+ const env = process.env.NODE_ENV || 'development';
14
+ return env !== 'development' && env !== 'local' && env !== 'dev';
15
+ })();
16
+
17
+ /**
18
+ * Validates that a repo path is allowed in the current mode.
19
+ * - In development: any path is allowed (for local folder browsing)
20
+ * - In production/SaaS: only paths under ALLOWED_ROOTS are permitted
21
+ *
22
+ * Returns null if valid, or a Response with a 403 error if blocked.
23
+ */
24
+ export function validateRepoPath(repoPath: string): Response | null {
25
+ if (!IS_PRODUCTION) return null; // Allow everything in dev
26
+
27
+ const resolved = path.resolve(repoPath);
28
+ const isAllowed = ALLOWED_ROOTS.some(root => resolved.startsWith(root + path.sep) || resolved === root);
29
+
30
+ if (!isAllowed) {
31
+ console.warn(`[security] Blocked access to path outside allowed roots: ${resolved}`);
32
+ return Response.json(
33
+ { error: 'Access denied: this path is not accessible in production mode.' },
34
+ { status: 403 }
35
+ );
36
+ }
37
+
38
+ return null; // Path is valid
39
+ }
40
+
41
+ /**
42
+ * Quick guard for routes that should be completely disabled in production.
43
+ * Returns a 403 Response if in production, null otherwise.
44
+ */
45
+ export function blockInProduction(routeName: string): Response | null {
46
+ if (!IS_PRODUCTION) return null;
47
+
48
+ console.warn(`[security] Blocked ${routeName} in production mode`);
49
+ return Response.json(
50
+ { error: `${routeName} is not available in production mode.` },
51
+ { status: 403 }
52
+ );
53
+ }
Binary file
Binary file
Binary file