Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

deps.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

deps.tsxBlame298 lines · 1 contributor
8098672Claude1/**
2 * Block J1 — Dependency graph routes.
3 *
4 * GET /:owner/:repo/dependencies — grouped list + summary
5 * POST /:owner/:repo/dependencies/reindex — owner-only, walk manifests
6 */
7
8import { Hono } from "hono";
9import { and, eq } from "drizzle-orm";
10import { db } from "../db";
11import { repositories, users } from "../db/schema";
12import { Layout } from "../views/layout";
13import { RepoHeader, RepoNav } from "../views/components";
14import { softAuth, requireAuth } from "../middleware/auth";
15import type { AuthEnv } from "../middleware/auth";
16import {
17 indexRepositoryDependencies,
18 listDependenciesForRepo,
19 summarizeDependencies,
20} from "../lib/deps";
b426348Claude21import { generateSpdx, generateCycloneDx } from "../lib/sbom";
8098672Claude22
23const deps = new Hono<AuthEnv>();
24deps.use("*", softAuth);
25
26async function loadRepo(ownerName: string, repoName: string) {
27 const [owner] = await db
28 .select()
29 .from(users)
30 .where(eq(users.username, ownerName))
31 .limit(1);
32 if (!owner) return null;
33 const [repo] = await db
34 .select()
35 .from(repositories)
36 .where(
37 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
38 )
39 .limit(1);
40 if (!repo) return null;
41 return { owner, repo };
42}
43
44// ---------- Overview ----------
45
46deps.get("/:owner/:repo/dependencies", async (c) => {
47 const user = c.get("user");
48 const { owner: ownerName, repo: repoName } = c.req.param();
49 const ctx = await loadRepo(ownerName, repoName);
50 if (!ctx) return c.notFound();
51 const { repo } = ctx;
52 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
53 return c.notFound();
54 }
55
56 const all = await listDependenciesForRepo(repo.id);
57 const summary = await summarizeDependencies(repo.id);
58 const isOwner = !!user && user.id === repo.ownerId;
59 const message = c.req.query("message");
60 const error = c.req.query("error");
61
62 // Group by ecosystem
63 const grouped = new Map<string, typeof all>();
64 for (const d of all) {
65 const list = grouped.get(d.ecosystem) || [];
66 list.push(d);
67 grouped.set(d.ecosystem, list);
68 }
69
70 return c.html(
71 <Layout
72 title={`Dependencies — ${ownerName}/${repoName}`}
73 user={user}
74 >
75 <RepoHeader owner={ownerName} repo={repoName} />
76 <RepoNav owner={ownerName} repo={repoName} active="code" />
77 <div class="settings-container">
78 <div style="display:flex;justify-content:space-between;align-items:center">
79 <h2 style="margin:0">Dependencies</h2>
b426348Claude80 <div style="display:flex;gap:8px;align-items:center">
81 {all.length > 0 && (
82 <>
83 <a href={`/${ownerName}/${repoName}/dependencies/sbom/spdx`} class="btn btn-sm" style="text-decoration:none">
84 SPDX
85 </a>
86 <a href={`/${ownerName}/${repoName}/dependencies/sbom/cyclonedx`} class="btn btn-sm" style="text-decoration:none">
87 CycloneDX
88 </a>
89 </>
90 )}
91 {isOwner && (
92 <form
93 method="POST"
94 action={`/${ownerName}/${repoName}/dependencies/reindex`}
95 style="margin:0"
96 >
97 <button type="submit" class="btn btn-primary btn-sm">
98 Reindex
99 </button>
100 </form>
101 )}
102 </div>
8098672Claude103 </div>
104 {message && (
105 <div class="auth-success" style="margin-top:12px">
106 {decodeURIComponent(message)}
107 </div>
108 )}
109 {error && (
110 <div class="auth-error" style="margin-top:12px">
111 {decodeURIComponent(error)}
112 </div>
113 )}
114 <p style="color:var(--text-muted);margin-top:8px">
115 Parsed from <code>package.json</code>, <code>requirements.txt</code>,{" "}
116 <code>pyproject.toml</code>, <code>go.mod</code>,{" "}
117 <code>Cargo.toml</code>, <code>Gemfile</code>, and{" "}
118 <code>composer.json</code> on the default branch. Transitive
119 dependencies are not resolved.
120 </p>
121
122 {all.length === 0 ? (
123 <div class="panel-empty" style="padding:24px">
124 No dependencies indexed yet.
125 {isOwner && " Click Reindex to scan the repository."}
126 </div>
127 ) : (
128 <>
129 <div
130 style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin:16px 0"
131 >
132 <div class="panel" style="padding:12px;text-align:center">
133 <div style="font-size:20px;font-weight:700">
134 {all.length}
135 </div>
136 <div
137 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
138 >
139 Dependencies
140 </div>
141 </div>
142 {summary.map((s) => (
143 <div class="panel" style="padding:12px;text-align:center">
144 <div style="font-size:20px;font-weight:700">
145 {s.count}
146 </div>
147 <div
148 style="font-size:11px;color:var(--text-muted);text-transform:uppercase"
149 >
150 {s.ecosystem}
151 </div>
152 </div>
153 ))}
154 </div>
155
156 {Array.from(grouped.entries()).map(([ecosystem, list]) => (
157 <>
158 <h3 style="margin-top:20px">
159 {ecosystem}{" "}
160 <span
161 style="font-size:12px;color:var(--text-muted);font-weight:400"
162 >
163 ({list.length})
164 </span>
165 </h3>
166 <div class="panel" style="margin-bottom:12px">
167 {list.map((d) => (
168 <div
169 class="panel-item"
170 style="justify-content:space-between;flex-wrap:wrap;gap:6px"
171 >
172 <div>
173 <span
174 style="font-family:var(--font-mono);font-weight:600"
175 >
176 {d.name}
177 </span>
178 {d.versionSpec && (
179 <span
180 style="margin-left:8px;font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
181 >
182 {d.versionSpec}
183 </span>
184 )}
185 {d.isDev && (
186 <span
187 style="margin-left:8px;font-size:10px;padding:2px 6px;background:var(--bg-subtle);border-radius:3px;color:var(--text-muted);text-transform:uppercase"
188 >
189 dev
190 </span>
191 )}
192 </div>
193 <a
194 href={`/${ownerName}/${repoName}/blob/HEAD/${d.manifestPath}`}
195 style="font-size:12px;color:var(--text-muted);font-family:var(--font-mono)"
196 >
197 {d.manifestPath}
198 </a>
199 </div>
200 ))}
201 </div>
202 </>
203 ))}
204 </>
205 )}
206 </div>
207 </Layout>
208 );
209});
210
211// ---------- Reindex (owner-only) ----------
212
213deps.post("/:owner/:repo/dependencies/reindex", requireAuth, async (c) => {
214 const user = c.get("user");
215 const { owner: ownerName, repo: repoName } = c.req.param();
216 const ctx = await loadRepo(ownerName, repoName);
217 if (!ctx) return c.notFound();
218 const { repo } = ctx;
219 if (!user || user.id !== repo.ownerId) {
220 return c.html(
221 <Layout title="Forbidden" user={user}>
222 <div class="empty-state">
223 <h2>403</h2>
224 <p>Only the repository owner can reindex dependencies.</p>
225 </div>
226 </Layout>,
227 403
228 );
229 }
230
231 const result = await indexRepositoryDependencies(repo.id);
232 const to = `/${ownerName}/${repoName}/dependencies`;
233 if (!result) {
234 return c.redirect(
235 `${to}?error=${encodeURIComponent(
236 "Reindex failed — is the default branch empty?"
237 )}`
238 );
239 }
240 return c.redirect(
241 `${to}?message=${encodeURIComponent(
242 `Indexed ${result.indexed} dependencies across ${result.manifests} manifests.`
243 )}`
244 );
245});
246
b426348Claude247// ---------- SBOM Export (SPDX) ----------
248
249deps.get("/:owner/:repo/dependencies/sbom/spdx", async (c) => {
250 const user = c.get("user");
251 const { owner: ownerName, repo: repoName } = c.req.param();
252 const ctx = await loadRepo(ownerName, repoName);
253 if (!ctx) return c.notFound();
254 const { repo } = ctx;
255 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) return c.notFound();
256
257 const spdx = await generateSpdx(repo.id, `${ownerName}/${repoName}`);
258 return c.json(spdx, 200, {
259 "Content-Disposition": `attachment; filename="${repoName}-sbom-spdx.json"`,
260 });
261});
262
263// ---------- SBOM Export (CycloneDX) ----------
264
265deps.get("/:owner/:repo/dependencies/sbom/cyclonedx", async (c) => {
266 const user = c.get("user");
267 const { owner: ownerName, repo: repoName } = c.req.param();
268 const ctx = await loadRepo(ownerName, repoName);
269 if (!ctx) return c.notFound();
270 const { repo } = ctx;
271 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) return c.notFound();
272
273 const bom = await generateCycloneDx(repo.id, `${ownerName}/${repoName}`);
274 return c.json(bom, 200, {
275 "Content-Disposition": `attachment; filename="${repoName}-sbom-cyclonedx.json"`,
276 });
277});
278
279// ---------- SBOM API (JSON) ----------
280
281deps.get("/api/repos/:owner/:repo/sbom", softAuth, async (c) => {
282 const user = c.get("user");
283 const { owner: ownerName, repo: repoName } = c.req.param();
284 const format = c.req.query("format") || "cyclonedx";
285 const ctx = await loadRepo(ownerName, repoName);
286 if (!ctx) return c.json({ error: "Not found" }, 404);
287 const { repo } = ctx;
288 if (repo.isPrivate && (!user || user.id !== repo.ownerId)) {
289 return c.json({ error: "Not found" }, 404);
290 }
291
292 if (format === "spdx") {
293 return c.json(await generateSpdx(repo.id, `${ownerName}/${repoName}`));
294 }
295 return c.json(await generateCycloneDx(repo.id, `${ownerName}/${repoName}`));
296});
297
8098672Claude298export default deps;