gitmaps 1.1.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 (121) hide show
  1. package/README.md +267 -118
  2. package/app/[...slug]/page.client.tsx +1 -0
  3. package/app/[...slug]/page.tsx +6 -0
  4. package/app/analytics.db +0 -0
  5. package/app/api/analytics/route.ts +64 -0
  6. package/app/api/auth/positions/route.ts +95 -33
  7. package/app/api/build-info/route.ts +19 -0
  8. package/app/api/chat/route.ts +13 -2
  9. package/app/api/og-image/route.ts +14 -0
  10. package/app/api/repo/file-content/route.ts +73 -20
  11. package/app/api/repo/load/route.test.ts +62 -0
  12. package/app/api/repo/load/route.ts +41 -1
  13. package/app/api/repo/pdf-thumb/route.ts +127 -0
  14. package/app/api/repo/resolve-slug/route.ts +51 -0
  15. package/app/api/repo/tree/route.ts +188 -104
  16. package/app/api/version/route.ts +26 -0
  17. package/app/globals.css +5706 -4938
  18. package/app/layout.tsx +1279 -490
  19. package/app/lib/auto-arrange.test.ts +158 -0
  20. package/app/lib/auto-arrange.ts +147 -0
  21. package/app/lib/canvas-export.ts +358 -358
  22. package/app/lib/canvas.ts +625 -564
  23. package/app/lib/cards.tsx +1361 -916
  24. package/app/lib/chat.tsx +65 -9
  25. package/app/lib/code-editor.ts +86 -2
  26. package/app/lib/context.test.ts +32 -0
  27. package/app/lib/context.ts +19 -3
  28. package/app/lib/cursor-sharing.ts +34 -0
  29. package/app/lib/events.tsx +71 -93
  30. package/app/lib/export-canvas.ts +287 -0
  31. package/app/lib/file-card-plugin.ts +148 -148
  32. package/app/lib/file-modal.tsx +49 -0
  33. package/app/lib/file-preview.ts +486 -427
  34. package/app/lib/github-import.test.ts +424 -0
  35. package/app/lib/initial-route-hydration.test.ts +283 -0
  36. package/app/lib/initial-route-hydration.ts +202 -0
  37. package/app/lib/landing-reset.test.ts +99 -0
  38. package/app/lib/landing-reset.ts +106 -0
  39. package/app/lib/landing-shell.test.ts +75 -0
  40. package/app/lib/large-repo-optimization.ts +37 -0
  41. package/app/lib/layout-snapshots.ts +320 -0
  42. package/app/lib/loading.test.ts +69 -0
  43. package/app/lib/loading.tsx +160 -45
  44. package/app/lib/mount-cleanup.test.ts +52 -0
  45. package/app/lib/mount-cleanup.ts +34 -0
  46. package/app/lib/mount-init.test.ts +123 -0
  47. package/app/lib/mount-init.ts +107 -0
  48. package/app/lib/mount-lifecycle.test.ts +39 -0
  49. package/app/lib/mount-lifecycle.ts +12 -0
  50. package/app/lib/mount-route-wiring.test.ts +87 -0
  51. package/app/lib/mount-route-wiring.ts +84 -0
  52. package/app/lib/multi-repo.ts +14 -0
  53. package/app/lib/onboarding-tutorial.ts +278 -0
  54. package/app/lib/positions.ts +190 -121
  55. package/app/lib/recent-commits.test.ts +869 -0
  56. package/app/lib/recent-commits.ts +227 -0
  57. package/app/lib/repo-handoff.test.ts +23 -0
  58. package/app/lib/repo-handoff.ts +16 -0
  59. package/app/lib/repo-progressive.ts +119 -0
  60. package/app/lib/repo-select.test.ts +61 -0
  61. package/app/lib/repo-select.ts +74 -0
  62. package/app/lib/repo.tsx +1383 -987
  63. package/app/lib/role.ts +228 -0
  64. package/app/lib/route-catchall.test.ts +27 -0
  65. package/app/lib/route-repo-entry.test.ts +95 -0
  66. package/app/lib/route-repo-entry.ts +36 -0
  67. package/app/lib/router-contract.test.ts +22 -0
  68. package/app/lib/router-contract.ts +19 -0
  69. package/app/lib/shared-layout.test.ts +86 -0
  70. package/app/lib/shared-layout.ts +82 -0
  71. package/app/lib/status-bar.test.ts +118 -0
  72. package/app/lib/status-bar.ts +365 -128
  73. package/app/lib/sync-controls.test.ts +43 -0
  74. package/app/lib/sync-controls.tsx +303 -0
  75. package/app/lib/test-dom.ts +145 -0
  76. package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
  77. package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
  78. package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
  79. package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
  80. package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
  81. package/app/lib/transclusion-smoke.test.ts +163 -0
  82. package/app/lib/tutorial.ts +301 -0
  83. package/app/lib/version.ts +93 -0
  84. package/app/lib/viewport-culling.ts +740 -735
  85. package/app/lib/virtual-files.ts +456 -0
  86. package/app/lib/webgl-text.ts +189 -0
  87. package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
  88. package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
  89. package/app/og-image.png +0 -0
  90. package/app/page.client.tsx +70 -269
  91. package/app/page.tsx +15 -16
  92. package/app/state/machine.js +13 -0
  93. package/package.json +16 -7
  94. package/server.ts +10 -0
  95. package/app/[owner]/[repo]/page.tsx +0 -6
  96. package/app/[slug]/page.tsx +0 -6
  97. package/packages/galaxydraw/README.md +0 -296
  98. package/packages/galaxydraw/banner.png +0 -0
  99. package/packages/galaxydraw/demo/build-static.ts +0 -100
  100. package/packages/galaxydraw/demo/client.ts +0 -154
  101. package/packages/galaxydraw/demo/dist/client.js +0 -8
  102. package/packages/galaxydraw/demo/index.html +0 -256
  103. package/packages/galaxydraw/demo/server.ts +0 -96
  104. package/packages/galaxydraw/dist/index.js +0 -984
  105. package/packages/galaxydraw/dist/index.js.map +0 -16
  106. package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
  107. package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
  108. package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
  109. package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
  110. package/packages/galaxydraw/package.json +0 -49
  111. package/packages/galaxydraw/perf.test.ts +0 -284
  112. package/packages/galaxydraw/src/core/cards.ts +0 -435
  113. package/packages/galaxydraw/src/core/engine.ts +0 -339
  114. package/packages/galaxydraw/src/core/events.ts +0 -81
  115. package/packages/galaxydraw/src/core/layout.ts +0 -136
  116. package/packages/galaxydraw/src/core/minimap.ts +0 -216
  117. package/packages/galaxydraw/src/core/state.ts +0 -177
  118. package/packages/galaxydraw/src/core/viewport.ts +0 -106
  119. package/packages/galaxydraw/src/galaxydraw.css +0 -166
  120. package/packages/galaxydraw/src/index.ts +0 -40
  121. package/packages/galaxydraw/tsconfig.json +0 -30
@@ -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
+ }
@@ -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,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
+ }
@@ -1,28 +1,81 @@
1
- import { measure } from 'measure-fn';
2
- import simpleGit from 'simple-git';
3
- import { validateRepoPath } from '../validate-path';
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
- return measure('api:repo:file-content', async () => {
7
- try {
8
- const { path: repoPath, commit, filePath } = await req.json();
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
- if (!repoPath || !commit || !filePath) {
11
- return new Response('Repository path, commit, and file path are required', { status: 400 });
12
- }
57
+ const blocked = validateRepoPath(repoPath);
58
+ if (blocked) return blocked;
13
59
 
14
- const blocked = validateRepoPath(repoPath);
15
- if (blocked) return blocked;
60
+ const ext = path.extname(file).toLowerCase();
61
+ const mimeType = MIME_TYPES[ext];
16
62
 
17
- const git = simpleGit(repoPath);
63
+ if (!mimeType) {
64
+ return new Response("Not an image file", { status: 400 });
65
+ }
18
66
 
19
- // Get file content at this commit
20
- const content = await git.show([`${commit}:${filePath}`]);
67
+ const fullPath = path.join(repoPath, file);
68
+ const buffer = readFileSync(fullPath);
21
69
 
22
- return Response.json({ content });
23
- } catch (error: any) {
24
- console.error('api:repo:file-content:error', error);
25
- return new Response(`Error: ${error.message}`, { status: 500 });
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
  }
@@ -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
- return Response.json({ commits });
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 });
@@ -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
+ }