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

repo-settings.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.

repo-settings.tsxBlame1457 lines · 1 contributor
79136bbClaude1/**
2 * Repository settings — description, visibility, default branch, danger zone.
3 */
4
5import { Hono } from "hono";
6import { eq, and } from "drizzle-orm";
7import { db } from "../db";
71cd5ecClaude8import { repositories, users, repoTransfers } from "../db/schema";
79136bbClaude9import { Layout } from "../views/layout";
10import { RepoHeader } from "../views/components";
11import { softAuth, requireAuth } from "../middleware/auth";
12import type { AuthEnv } from "../middleware/auth";
febd4f0Claude13import { requireRepoAccess } from "../middleware/repo-access";
79136bbClaude14import { listBranches } from "../git/repository";
534f04aClaude15import { audit } from "../lib/notify";
79136bbClaude16import { rm } from "fs/promises";
58307aeClaude17import { EmptyState } from "../views/ui";
79136bbClaude18
19const repoSettings = new Hono<AuthEnv>();
20
21repoSettings.use("*", softAuth);
22
58307aeClaude23// Inline, scoped CSS — every class prefixed `.repo-settings-` so styles cannot
24// bleed into other surfaces. Pattern mirrors the user-settings polish
25// (commit 98eb360) and the admin-panel polish (commit 07f4b70).
26const repoSettingsStyles = `
27 .repo-settings-container { max-width: 880px; margin: 0 auto; padding: 0 var(--space-3); }
28
29 /* ─── Hero ─── */
30 .repo-settings-hero {
31 position: relative;
32 margin: var(--space-5) 0 var(--space-5);
33 padding: var(--space-5) var(--space-6);
34 background: var(--bg-elevated);
35 border: 1px solid var(--border);
36 border-radius: 16px;
37 overflow: hidden;
38 }
39 .repo-settings-hero::before {
40 content: '';
41 position: absolute;
42 top: 0; left: 0; right: 0;
43 height: 2px;
44 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
45 opacity: 0.7;
46 pointer-events: none;
47 }
48 .repo-settings-hero-bg {
49 position: absolute;
50 inset: -20% -10% auto auto;
51 width: 360px; height: 360px;
52 pointer-events: none;
53 z-index: 0;
54 }
55 .repo-settings-hero-orb {
56 position: absolute;
57 inset: 0;
58 background: radial-gradient(circle, rgba(140,109,255,0.18), rgba(54,197,214,0.09) 45%, transparent 70%);
59 filter: blur(80px);
60 opacity: 0.65;
61 animation: repoSettingsHeroOrb 14s ease-in-out infinite;
62 }
63 @keyframes repoSettingsHeroOrb {
64 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.55; }
65 50% { transform: scale(1.08) translate(-8px, 6px); opacity: 0.78; }
66 }
67 @media (prefers-reduced-motion: reduce) {
68 .repo-settings-hero-orb { animation: none; }
69 }
70 .repo-settings-hero-inner {
71 position: relative;
72 z-index: 1;
73 max-width: 640px;
74 }
75 .repo-settings-hero-eyebrow {
76 font-size: 13px;
77 color: var(--text-muted);
78 margin-bottom: var(--space-2);
79 letter-spacing: -0.005em;
80 }
81 .repo-settings-hero-eyebrow .repo-settings-hero-repo {
82 color: var(--accent);
83 font-weight: 600;
84 }
85 .repo-settings-hero-title {
86 font-size: clamp(28px, 4vw, 40px);
87 font-family: var(--font-display);
88 font-weight: 800;
89 letter-spacing: -0.028em;
90 line-height: 1.05;
91 margin: 0 0 var(--space-2);
92 color: var(--text-strong);
93 }
94 .repo-settings-hero-title .gradient-text {
95 background-image: linear-gradient(135deg, #a48bff 0%, #8c6dff 50%, #36c5d6 100%);
96 -webkit-background-clip: text;
97 background-clip: text;
98 -webkit-text-fill-color: transparent;
99 color: transparent;
100 }
101 .repo-settings-hero-sub {
102 font-size: 15px;
103 color: var(--text-muted);
104 margin: 0;
105 line-height: 1.5;
106 }
107 .repo-settings-hero-link {
108 display: inline-flex;
109 align-items: center;
110 gap: 6px;
111 margin-top: var(--space-3);
112 font-size: 13.5px;
113 color: var(--text-link, var(--accent));
114 text-decoration: none;
115 font-weight: 500;
116 }
117 .repo-settings-hero-link:hover { text-decoration: underline; }
118 .repo-settings-hero-link .arrow { transition: transform 120ms ease; }
119 .repo-settings-hero-link:hover .arrow { transform: translateX(2px); }
120
121 /* ─── Banners (success / error) ─── */
122 .repo-settings-banner {
123 display: flex;
124 align-items: center;
125 gap: 10px;
126 padding: 12px 16px;
127 border-radius: 12px;
128 font-size: 13.5px;
129 margin-bottom: var(--space-4);
130 line-height: 1.5;
131 }
132 .repo-settings-banner-success {
133 background: rgba(52,211,153,0.08);
134 color: #6ee7b7;
135 box-shadow: inset 0 0 0 1px rgba(52,211,153,0.30);
136 }
137 .repo-settings-banner-error {
138 background: rgba(248,113,113,0.08);
139 color: #fca5a5;
140 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.30);
141 }
142 .repo-settings-banner-icon {
143 width: 18px; height: 18px;
144 border-radius: 9999px;
145 flex-shrink: 0;
146 display: inline-flex;
147 align-items: center;
148 justify-content: center;
149 font-size: 12px;
150 font-weight: 700;
151 }
152 .repo-settings-banner-success .repo-settings-banner-icon {
153 background: rgba(52,211,153,0.18);
154 color: #34d399;
155 }
156 .repo-settings-banner-error .repo-settings-banner-icon {
157 background: rgba(248,113,113,0.18);
158 color: #f87171;
159 }
160
161 /* ─── Section cards ─── */
162 .repo-settings-section {
163 background: var(--bg-elevated);
164 border: 1px solid var(--border);
165 border-radius: 14px;
166 margin-bottom: var(--space-5);
167 overflow: hidden;
168 }
169 .repo-settings-section-head {
170 padding: var(--space-4) var(--space-5) var(--space-3);
171 border-bottom: 1px solid var(--border);
172 }
173 .repo-settings-section-eyebrow {
174 font-size: 11px;
175 font-weight: 600;
176 letter-spacing: 0.08em;
177 text-transform: uppercase;
178 color: var(--accent);
179 margin-bottom: 6px;
180 }
181 .repo-settings-section-title {
182 font-family: var(--font-display);
183 font-size: 18px;
184 font-weight: 700;
185 letter-spacing: -0.018em;
186 margin: 0 0 4px;
187 color: var(--text-strong);
188 }
189 .repo-settings-section-desc {
190 font-size: 13.5px;
191 color: var(--text-muted);
192 margin: 0;
193 line-height: 1.5;
194 }
195 .repo-settings-section-body { padding: var(--space-4) var(--space-5); }
196 .repo-settings-section-foot {
197 padding: var(--space-3) var(--space-5);
198 border-top: 1px solid var(--border);
199 background: rgba(255,255,255,0.012);
200 display: flex;
201 justify-content: flex-end;
202 gap: var(--space-2);
203 align-items: center;
204 flex-wrap: wrap;
205 }
206 .repo-settings-section-foot .repo-settings-foot-hint {
207 margin-right: auto;
208 font-size: 12.5px;
209 color: var(--text-muted);
210 }
211
212 /* ─── Form rows ─── */
213 .repo-settings-field { margin-bottom: var(--space-4); }
214 .repo-settings-field:last-child { margin-bottom: 0; }
215 .repo-settings-field-label {
216 display: block;
217 font-size: 13px;
218 font-weight: 600;
219 color: var(--text-strong);
220 margin-bottom: 6px;
221 letter-spacing: -0.005em;
222 }
223 .repo-settings-field-hint {
224 font-size: 12.5px;
225 color: var(--text-muted);
226 margin-top: 6px;
227 line-height: 1.45;
228 }
229 .repo-settings-input,
230 .repo-settings-select {
231 width: 100%;
232 padding: 9px 12px;
233 font-size: 14px;
234 color: var(--text);
235 background: var(--bg);
236 border: 1px solid var(--border-strong, var(--border));
237 border-radius: 8px;
238 outline: none;
239 transition: border-color 120ms ease, box-shadow 120ms ease;
240 font-family: var(--font-sans);
241 }
242 .repo-settings-input:focus,
243 .repo-settings-select:focus {
244 border-color: var(--border-focus, var(--accent));
245 box-shadow: 0 0 0 3px rgba(140,109,255,0.18);
246 }
247
248 /* ─── Visibility radio cards ─── */
249 .repo-settings-visibility {
250 display: grid;
251 grid-template-columns: 1fr 1fr;
252 gap: var(--space-2);
253 }
254 @media (max-width: 600px) {
255 .repo-settings-visibility { grid-template-columns: 1fr; }
256 }
257 .repo-settings-vis-card {
258 display: flex;
259 gap: 10px;
260 padding: 14px;
261 background: var(--bg);
262 border: 1px solid var(--border);
263 border-radius: 12px;
264 cursor: pointer;
265 transition: border-color 120ms ease, background 120ms ease;
266 }
267 .repo-settings-vis-card:hover { border-color: var(--border-strong, var(--border)); }
268 .repo-settings-vis-card:has(input:checked) {
269 border-color: rgba(140,109,255,0.55);
270 background: rgba(140,109,255,0.06);
271 box-shadow: 0 0 0 1px rgba(140,109,255,0.25);
272 }
273 .repo-settings-vis-radio {
274 margin-top: 3px;
275 accent-color: var(--accent);
276 }
277 .repo-settings-vis-body { display: flex; flex-direction: column; gap: 4px; }
278 .repo-settings-vis-label {
279 font-size: 14px;
280 font-weight: 600;
281 color: var(--text-strong);
282 }
283 .repo-settings-vis-desc {
284 font-size: 12.5px;
285 color: var(--text-muted);
286 line-height: 1.4;
287 }
288
289 /* ─── Toggle rows (stale-sweep) ─── */
290 .repo-settings-toggle-row {
291 display: flex;
292 align-items: flex-start;
293 gap: var(--space-3);
294 padding: 12px 14px;
295 border-radius: 10px;
296 border: 1px solid var(--border-subtle, var(--border));
297 background: var(--bg-secondary, var(--bg));
298 margin-bottom: 8px;
299 transition: border-color 120ms ease, background 120ms ease;
300 cursor: pointer;
301 }
302 .repo-settings-toggle-row:last-of-type { margin-bottom: 0; }
303 .repo-settings-toggle-row:hover {
304 border-color: var(--border);
305 background: rgba(255,255,255,0.02);
306 }
307 .repo-settings-toggle-row:has(input:checked) {
308 border-color: rgba(140,109,255,0.45);
309 background: rgba(140,109,255,0.05);
310 }
311 .repo-settings-toggle-row input[type="checkbox"] {
312 margin-top: 2px;
313 flex-shrink: 0;
314 width: 16px; height: 16px;
315 accent-color: var(--accent);
316 cursor: pointer;
317 }
318 .repo-settings-toggle-text {
319 flex: 1;
320 font-size: 14px;
321 color: var(--text);
322 line-height: 1.45;
323 }
324 .repo-settings-toggle-text-title {
325 display: block;
326 font-weight: 600;
327 color: var(--text-strong);
328 margin-bottom: 2px;
329 }
330 .repo-settings-toggle-text-hint {
331 display: block;
332 margin-top: 3px;
333 font-size: 12.5px;
334 color: var(--text-muted);
335 }
336
337 /* ─── Primary CTA ─── */
338 .repo-settings-cta {
339 display: inline-flex;
340 align-items: center;
341 gap: 6px;
342 padding: 9px 16px;
343 font-size: 14px;
344 font-weight: 600;
345 color: white;
346 background: linear-gradient(135deg, #8c6dff 0%, #5b7bff 100%);
347 border: none;
348 border-radius: 8px;
349 cursor: pointer;
350 transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease;
351 text-decoration: none;
352 font-family: inherit;
353 }
354 .repo-settings-cta:hover {
355 filter: brightness(1.08);
356 box-shadow: 0 6px 18px rgba(140,109,255,0.25);
357 transform: translateY(-1px);
358 }
359 .repo-settings-cta .arrow { transition: transform 120ms ease; }
360 .repo-settings-cta:hover .arrow { transform: translateX(2px); }
361 .repo-settings-cta-secondary {
362 display: inline-flex;
363 align-items: center;
364 gap: 6px;
365 padding: 8px 14px;
366 font-size: 13.5px;
367 font-weight: 500;
368 color: var(--text);
369 background: var(--bg);
370 border: 1px solid var(--border-strong, var(--border));
371 border-radius: 8px;
372 cursor: pointer;
373 text-decoration: none;
374 transition: border-color 120ms ease, background 120ms ease;
375 font-family: inherit;
376 }
377 .repo-settings-cta-secondary:hover {
378 border-color: var(--accent);
379 background: rgba(140,109,255,0.06);
380 }
381
382 /* ─── Inline transfer row ─── */
383 .repo-settings-inline-row {
384 display: flex;
385 gap: var(--space-2);
386 align-items: stretch;
387 flex-wrap: wrap;
388 }
389 .repo-settings-inline-row .repo-settings-input { flex: 1; min-width: 220px; }
390
391 /* ─── Status pill (template / archived) ─── */
392 .repo-settings-pill {
393 display: inline-flex;
394 align-items: center;
395 gap: 6px;
396 padding: 4px 10px;
397 border-radius: 9999px;
398 font-size: 11.5px;
399 font-weight: 600;
400 letter-spacing: 0.01em;
401 margin-bottom: var(--space-2);
402 }
403 .repo-settings-pill.is-on {
404 background: rgba(140,109,255,0.14);
405 color: #b69dff;
406 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.35);
407 }
408 .repo-settings-pill.is-off {
409 background: rgba(255,255,255,0.04);
410 color: var(--text-muted);
411 box-shadow: inset 0 0 0 1px var(--border);
412 }
413 .repo-settings-pill .dot {
414 width: 6px; height: 6px;
415 border-radius: 9999px;
416 background: currentColor;
417 box-shadow: 0 0 8px currentColor;
418 }
419 .repo-settings-pill.is-off .dot { box-shadow: none; opacity: 0.6; }
420
421 /* ─── Danger zone ─── */
422 .repo-settings-danger {
423 position: relative;
424 margin-top: var(--space-6);
425 padding: 0;
426 border: 1px solid rgba(248,113,113,0.30);
427 border-radius: 14px;
428 background:
429 linear-gradient(180deg, rgba(248,113,113,0.05) 0%, rgba(248,113,113,0.02) 100%),
430 var(--bg-elevated);
431 overflow: hidden;
432 }
433 .repo-settings-danger::before {
434 content: '';
435 position: absolute;
436 top: 0; left: 0; right: 0;
437 height: 2px;
438 background: linear-gradient(90deg, transparent 0%, #f87171 30%, #ffb45e 70%, transparent 100%);
439 opacity: 0.7;
440 pointer-events: none;
441 }
442 .repo-settings-danger-head {
443 padding: var(--space-4) var(--space-5) var(--space-3);
444 border-bottom: 1px solid rgba(248,113,113,0.15);
445 }
446 .repo-settings-danger-eyebrow {
447 font-size: 11px;
448 font-weight: 600;
449 letter-spacing: 0.08em;
450 text-transform: uppercase;
451 color: #f87171;
452 margin-bottom: 6px;
453 }
454 .repo-settings-danger-title {
455 font-family: var(--font-display);
456 font-size: 18px;
457 font-weight: 700;
458 letter-spacing: -0.018em;
459 margin: 0 0 4px;
460 color: var(--text-strong);
461 }
462 .repo-settings-danger-desc {
463 font-size: 13.5px;
464 color: var(--text-muted);
465 margin: 0;
466 line-height: 1.5;
467 }
468 .repo-settings-danger-body { padding: var(--space-4) var(--space-5); }
469 .repo-settings-danger-body p { margin: 0 0 var(--space-3); font-size: 14px; line-height: 1.55; }
470 .repo-settings-danger-body p:last-child { margin-bottom: 0; }
471 .repo-settings-danger-body p.muted { color: var(--text-muted); font-size: 13px; }
472 .repo-settings-danger-btn {
473 display: inline-flex;
474 align-items: center;
475 gap: 6px;
476 padding: 9px 16px;
477 font-size: 14px;
478 font-weight: 600;
479 color: #fff;
480 background: linear-gradient(135deg, #f87171 0%, #ef4444 100%);
481 border: none;
482 border-radius: 8px;
483 cursor: pointer;
484 font-family: inherit;
485 transition: filter 120ms ease, box-shadow 120ms ease, transform 120ms ease;
486 }
487 .repo-settings-danger-btn:hover {
488 filter: brightness(1.08);
489 box-shadow: 0 6px 18px rgba(248,113,113,0.25);
490 transform: translateY(-1px);
491 }
492
493 /* ─── Responsive ─── */
494 @media (max-width: 720px) {
495 .repo-settings-hero { padding: var(--space-4) var(--space-4); }
496 .repo-settings-section-head,
497 .repo-settings-section-body,
498 .repo-settings-section-foot,
499 .repo-settings-danger-head,
500 .repo-settings-danger-body { padding-left: var(--space-4); padding-right: var(--space-4); }
501 }
502`;
503
504/** Hero header for the repo settings page. */
505function RepoSettingsHero(props: { owner: string; repo: string }) {
506 const { owner, repo } = props;
507 return (
508 <div class="repo-settings-hero">
509 <div class="repo-settings-hero-bg" aria-hidden="true">
510 <div class="repo-settings-hero-orb" />
511 </div>
512 <div class="repo-settings-hero-inner">
513 <div class="repo-settings-hero-eyebrow">
514 Repository settings ·{" "}
515 <span class="repo-settings-hero-repo">
516 {owner}/{repo}
517 </span>
518 </div>
519 <h1 class="repo-settings-hero-title">
520 <span class="gradient-text">Configure</span>.
521 </h1>
522 <p class="repo-settings-hero-sub">
523 Description, visibility, branches, automation. Owners only.
524 </p>
525 <a
526 href={`/${owner}/${repo}/settings/collaborators`}
527 class="repo-settings-hero-link"
528 >
529 Manage collaborators <span class="arrow">→</span>
530 </a>
531 </div>
532 </div>
533 );
534}
535
536function Banner(props: { kind: "success" | "error"; text: string }) {
537 return (
538 <div
539 class={`repo-settings-banner repo-settings-banner-${props.kind}`}
540 role="status"
541 >
542 <span class="repo-settings-banner-icon" aria-hidden="true">
543 {props.kind === "success" ? "✓" : "!"}
544 </span>
545 <span>{props.text}</span>
546 </div>
547 );
548}
549
79136bbClaude550// Settings page
febd4f0Claude551repoSettings.get("/:owner/:repo/settings", requireAuth, requireRepoAccess("admin"), async (c) => {
79136bbClaude552 const { owner: ownerName, repo: repoName } = c.req.param();
553 const user = c.get("user")!;
554 const success = c.req.query("success");
555 const error = c.req.query("error");
556
557 const [owner] = await db
558 .select()
559 .from(users)
560 .where(eq(users.username, ownerName))
561 .limit(1);
562
563 if (!owner || owner.id !== user.id) {
564 return c.html(
565 <Layout title="Unauthorized" user={user}>
bb0f894Claude566 <EmptyState title="Unauthorized">
79136bbClaude567 <p>Only the repository owner can access settings.</p>
bb0f894Claude568 </EmptyState>
79136bbClaude569 </Layout>,
570 403
571 );
572 }
573
574 const [repo] = await db
575 .select()
576 .from(repositories)
577 .where(
578 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
579 )
580 .limit(1);
581
582 if (!repo) return c.notFound();
583
584 const branches = await listBranches(ownerName, repoName);
585
586 return c.html(
587 <Layout title={`Settings — ${ownerName}/${repoName}`} user={user}>
588 <RepoHeader owner={ownerName} repo={repoName} />
58307aeClaude589 <style dangerouslySetInnerHTML={{ __html: repoSettingsStyles }} />
590 <div class="repo-settings-container">
591 <RepoSettingsHero owner={ownerName} repo={repoName} />
592
79136bbClaude593 {success && (
58307aeClaude594 <Banner kind="success" text={decodeURIComponent(success)} />
79136bbClaude595 )}
58307aeClaude596 {error && <Banner kind="error" text={decodeURIComponent(error)} />}
79136bbClaude597
58307aeClaude598 {/* ─── General: description + default branch + visibility ─── */}
599 <section class="repo-settings-section">
600 <div class="repo-settings-section-head">
601 <div class="repo-settings-section-eyebrow">General</div>
602 <h2 class="repo-settings-section-title">Repository basics</h2>
603 <p class="repo-settings-section-desc">
604 The headline details visitors see — description, the default
605 branch they land on, and who can browse the code.
606 </p>
607 </div>
608 <form
609 method="post"
610 action={`/${ownerName}/${repoName}/settings`}
611 >
612 <div class="repo-settings-section-body">
613 <div class="repo-settings-field">
614 <label class="repo-settings-field-label" for="description">
615 Description
616 </label>
79136bbClaude617 <input
58307aeClaude618 class="repo-settings-input"
619 name="description"
620 id="description"
621 value={repo.description || ""}
622 placeholder="A short description"
79136bbClaude623 />
58307aeClaude624 <div class="repo-settings-field-hint">
625 Shown on the repo home, search results, and explore pages.
626 </div>
627 </div>
628 <div class="repo-settings-field">
629 <label
630 class="repo-settings-field-label"
631 for="default_branch"
632 >
633 Default branch
634 </label>
635 <select
636 class="repo-settings-select"
637 name="default_branch"
638 id="default_branch"
639 >
640 {branches.length === 0 ? (
641 <option value={repo.defaultBranch}>
642 {repo.defaultBranch}
643 </option>
644 ) : (
645 branches.map((b) => (
646 <option value={b} selected={b === repo.defaultBranch}>
647 {b}
648 </option>
649 ))
650 )}
651 </select>
652 <div class="repo-settings-field-hint">
653 Where pulls land, where compare views start, and what
654 clones check out by default.
655 </div>
656 </div>
657 <div class="repo-settings-field">
658 <label class="repo-settings-field-label">Visibility</label>
659 <div class="repo-settings-visibility">
660 <label class="repo-settings-vis-card">
661 <input
662 type="radio"
663 name="visibility"
664 value="public"
665 checked={!repo.isPrivate}
666 class="repo-settings-vis-radio"
667 aria-label="Public"
668 />
669 <span class="repo-settings-vis-body">
670 <span class="repo-settings-vis-label">Public</span>
671 <span class="repo-settings-vis-desc">
672 Anyone can see this repository. You choose who can
673 commit.
674 </span>
675 </span>
676 </label>
677 <label class="repo-settings-vis-card">
678 <input
679 type="radio"
680 name="visibility"
681 value="private"
682 checked={repo.isPrivate}
683 class="repo-settings-vis-radio"
684 aria-label="Private"
685 />
686 <span class="repo-settings-vis-body">
687 <span class="repo-settings-vis-label">Private</span>
688 <span class="repo-settings-vis-desc">
689 Only you (and collaborators you invite) can see this
690 repository.
691 </span>
692 </span>
693 </label>
694 </div>
695 </div>
79136bbClaude696 </div>
58307aeClaude697 <div class="repo-settings-section-foot">
698 <button type="submit" class="repo-settings-cta">
699 Save changes <span class="arrow">→</span>
700 </button>
701 </div>
702 </form>
703 </section>
14c3cc8Claude704
58307aeClaude705 {/* ─── Spec to PR ─── */}
706 <section class="repo-settings-section">
707 <div class="repo-settings-section-head">
708 <div class="repo-settings-section-eyebrow">
709 Automation · experimental
710 </div>
711 <h2 class="repo-settings-section-title">Spec to PR</h2>
712 <p class="repo-settings-section-desc">
713 Paste a plain-English feature spec and let Claude draft a pull
714 request for you. PRs open as drafts — review every line before
715 merging.
716 </p>
717 </div>
718 <div class="repo-settings-section-body">
719 <a
720 href={`/${ownerName}/${repoName}/spec`}
721 class="repo-settings-cta"
722 >
723 Open Spec to PR <span class="arrow">→</span>
724 </a>
725 </div>
726 </section>
727
728 {/* ─── Template repository ─── */}
729 <section class="repo-settings-section">
730 <div class="repo-settings-section-head">
731 <div class="repo-settings-section-eyebrow">Template</div>
732 <h2 class="repo-settings-section-title">Template repository</h2>
733 <p class="repo-settings-section-desc">
71cd5ecClaude734 {repo.isTemplate
58307aeClaude735 ? "This repository is a template. Users can click “Use this template” to create a new repository with the same files."
736 : "Mark this repository as a template so others can seed new repositories from its files."}
737 </p>
738 </div>
739 <div class="repo-settings-section-body">
740 <span
741 class={`repo-settings-pill ${repo.isTemplate ? "is-on" : "is-off"}`}
742 >
743 <span class="dot" aria-hidden="true" />
744 {repo.isTemplate ? "Template enabled" : "Not a template"}
745 </span>
746 <form
747 method="post"
748 action={`/${ownerName}/${repoName}/settings/template`}
749 style="margin-top: var(--space-3)"
750 >
751 <input
752 type="hidden"
753 name="template"
754 value={repo.isTemplate ? "0" : "1"}
755 />
756 <button type="submit" class="repo-settings-cta-secondary">
757 {repo.isTemplate ? "Unmark as template" : "Mark as template"}
758 </button>
759 </form>
760 </div>
761 </section>
71cd5ecClaude762
58307aeClaude763 {/* ─── Transfer ownership ─── */}
764 <section class="repo-settings-section">
765 <div class="repo-settings-section-head">
766 <div class="repo-settings-section-eyebrow">Ownership</div>
767 <h2 class="repo-settings-section-title">Transfer ownership</h2>
768 <p class="repo-settings-section-desc">
769 Hand this repository to another user. The new owner can accept
770 or decline the transfer by viewing it.
771 </p>
772 </div>
71cd5ecClaude773 <form
e7e240eClaude774 method="post"
71cd5ecClaude775 action={`/${ownerName}/${repoName}/settings/transfer`}
776 onsubmit="return confirm('Transfer this repository? The new owner will have full control.')"
777 >
58307aeClaude778 <div class="repo-settings-section-body">
779 <div class="repo-settings-field">
780 <label class="repo-settings-field-label" for="new_owner">
781 New owner username
782 </label>
783 <div class="repo-settings-inline-row">
784 <input
785 type="text"
786 name="new_owner"
787 id="new_owner"
788 class="repo-settings-input"
789 placeholder="new-owner-username"
790 required
791 aria-label="New owner username"
792 />
793 <button type="submit" class="repo-settings-cta-secondary">
794 Transfer
795 </button>
796 </div>
797 <div class="repo-settings-field-hint">
798 Transfers are immediate. The new owner can rename or delete
799 the repository at any time.
800 </div>
801 </div>
802 </div>
71cd5ecClaude803 </form>
58307aeClaude804 </section>
71cd5ecClaude805
58307aeClaude806 {/* ─── Archive ─── */}
807 <section class="repo-settings-section">
808 <div class="repo-settings-section-head">
809 <div class="repo-settings-section-eyebrow">Lifecycle</div>
810 <h2 class="repo-settings-section-title">
811 {repo.isArchived ? "Unarchive repository" : "Archive repository"}
812 </h2>
813 <p class="repo-settings-section-desc">
814 {repo.isArchived
815 ? "This repository is archived and read-only. Unarchive to allow pushes and issue/PR activity again."
816 : "Mark this repository as archived. It becomes read-only — no pushes, no new issues or PRs. You can unarchive at any time."}
817 </p>
818 </div>
819 <div class="repo-settings-section-body">
820 <span
821 class={`repo-settings-pill ${repo.isArchived ? "is-on" : "is-off"}`}
534f04aClaude822 >
58307aeClaude823 <span class="dot" aria-hidden="true" />
824 {repo.isArchived ? "Archived" : "Active"}
825 </span>
826 <form
827 method="post"
828 action={`/${ownerName}/${repoName}/settings/archive`}
829 style="margin-top: var(--space-3)"
534f04aClaude830 >
831 <input
58307aeClaude832 type="hidden"
833 name="archive"
834 value={repo.isArchived ? "0" : "1"}
534f04aClaude835 />
58307aeClaude836 <button type="submit" class="repo-settings-cta-secondary">
837 {repo.isArchived ? "Unarchive" : "Archive"} this repository
838 </button>
839 </form>
840 </div>
841 </section>
534f04aClaude842
1d4ff60Claude843 {/* ─── AI test generator ─── */}
844 <section class="repo-settings-section">
845 <div class="repo-settings-section-head">
846 <div class="repo-settings-section-eyebrow">AI tests</div>
847 <h2 class="repo-settings-section-title">Auto-generate tests on PR open</h2>
848 <p class="repo-settings-section-desc">
849 When a pull request opens, Gluecron AI reads the diff and writes
850 tests for the new code, matching whatever framework your repo
851 already uses. Tests land on the same branch. Default off —
852 opt in here.
853 </p>
854 </div>
855 <form
856 method="post"
857 action={`/${ownerName}/${repoName}/settings/ai-tests`}
858 >
859 <div class="repo-settings-section-body">
860 <label
861 class="repo-settings-toggle-row"
862 aria-label="Auto-generate tests when a PR opens"
863 >
864 <input
865 type="checkbox"
866 name="auto_generate_tests"
867 value="1"
868 checked={repo.autoGenerateTests}
869 />
870 <span class="repo-settings-toggle-text">
871 <span class="repo-settings-toggle-text-title">
872 Auto-generate tests on PR open
873 </span>
874 <span class="repo-settings-toggle-text-hint">
875 Requires <code>ANTHROPIC_API_KEY</code>. Skips PRs without
876 source-file changes and PRs already opened by AI.
877 </span>
878 </span>
879 </label>
880 </div>
881 <div class="repo-settings-section-foot">
882 <button type="submit" class="repo-settings-cta">
883 Save AI test settings <span class="arrow">→</span>
884 </button>
885 </div>
886 </form>
887 </section>
888
9b3a183Claude889 {/* ─── Cloud dev environments ─── */}
890 <section
891 id="dev-envs"
892 class="repo-settings-section"
893 >
894 <div class="repo-settings-section-head">
895 <div class="repo-settings-section-eyebrow">Dev environments</div>
896 <h2 class="repo-settings-section-title">Enable cloud dev environments</h2>
897 <p class="repo-settings-section-desc">
898 When enabled, anyone with read access can hit{" "}
899 <code>/{ownerName}/{repoName}/dev</code> to spin up a
900 hosted VS Code IDE in the browser, backed by a cold-start
901 container. We read <code>.gluecron/dev.yml</code> for the
902 image + install commands; idle envs stop themselves after
903 30 minutes. Default off — each env burns a container.
904 </p>
905 </div>
906 <form
907 method="post"
908 action={`/${ownerName}/${repoName}/settings/dev-envs`}
909 >
910 <div class="repo-settings-section-body">
911 <label
912 class="repo-settings-toggle-row"
913 aria-label="Enable cloud dev environments for this repo"
914 >
915 <input
916 type="checkbox"
917 name="dev_envs_enabled"
918 value="1"
919 checked={
920 (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ??
921 false
922 }
923 />
924 <span class="repo-settings-toggle-text">
925 <span class="repo-settings-toggle-text-title">
926 Enable dev environments
927 </span>
928 <span class="repo-settings-toggle-text-hint">
929 Surfaces the <code>/{ownerName}/{repoName}/dev</code>{" "}
930 route. Commit <code>.gluecron/dev.yml</code> to your
931 repo to customise the image, ports, and extensions.
932 </span>
933 </span>
934 </label>
935 </div>
936 <div class="repo-settings-section-foot">
937 <button type="submit" class="repo-settings-cta">
938 Save dev env settings <span class="arrow">→</span>
939 </button>
940 </div>
941 </form>
942 </section>
943
58307aeClaude944 {/* ─── Stale activity ─── */}
945 <section class="repo-settings-section">
946 <div class="repo-settings-section-head">
947 <div class="repo-settings-section-eyebrow">Stale sweep</div>
948 <h2 class="repo-settings-section-title">Stale activity</h2>
949 <p class="repo-settings-section-desc">
950 Autopilot pokes PRs and issues that have gone quiet, then offers
951 a one-click close path. Each toggle controls the final close
952 step — pokes always happen, but they&apos;re harmless reminders.
953 </p>
954 </div>
79136bbClaude955 <form
e7e240eClaude956 method="post"
58307aeClaude957 action={`/${ownerName}/${repoName}/settings/stale`}
79136bbClaude958 >
58307aeClaude959 <div class="repo-settings-section-body">
960 <label
961 class="repo-settings-toggle-row"
962 aria-label="Auto-close stale PRs after 14 days of no activity post-poke"
963 >
964 <input
965 type="checkbox"
966 name="auto_close_stale_prs"
967 value="1"
968 checked={repo.autoCloseStalePrs}
969 />
970 <span class="repo-settings-toggle-text">
971 <span class="repo-settings-toggle-text-title">
972 Auto-close stale PRs
973 </span>
974 <span class="repo-settings-toggle-text-hint">
975 Close PRs that go quiet for 14 days after autopilot pokes
976 them.
977 </span>
978 </span>
979 </label>
980 <label
981 class="repo-settings-toggle-row"
982 aria-label="Auto-close stale issues after 60 days of no activity post-poke"
983 >
984 <input
985 type="checkbox"
986 name="auto_close_stale_issues"
987 value="1"
988 checked={repo.autoCloseStaleIssues}
989 />
990 <span class="repo-settings-toggle-text">
991 <span class="repo-settings-toggle-text-title">
992 Auto-close stale issues
993 </span>
994 <span class="repo-settings-toggle-text-hint">
995 Close issues that go quiet for 60 days after autopilot
996 pokes them.
997 </span>
998 </span>
999 </label>
1000 </div>
1001 <div class="repo-settings-section-foot">
1002 <button type="submit" class="repo-settings-cta">
1003 Save stale settings <span class="arrow">→</span>
1004 </button>
1005 </div>
79136bbClaude1006 </form>
58307aeClaude1007 </section>
1008
1009 {/* ─── Danger zone ─── */}
1010 <section class="repo-settings-danger">
1011 <div class="repo-settings-danger-head">
1012 <div class="repo-settings-danger-eyebrow">Danger zone</div>
1013 <h2 class="repo-settings-danger-title">Delete this repository</h2>
1014 <p class="repo-settings-danger-desc">
1015 Permanently remove this repository and every byte of its history,
1016 issues, PRs, stars, and webhooks. There is no undo.
1017 </p>
1018 </div>
1019 <div class="repo-settings-danger-body">
1020 <p class="muted">
1021 Once deleted, the URL <code>{ownerName}/{repoName}</code> frees
1022 up immediately. Open clones will fail to fetch or push.
1023 </p>
1024 <form
1025 method="post"
1026 action={`/${ownerName}/${repoName}/settings/delete`}
1027 onsubmit="return confirm('Are you sure? This cannot be undone.')"
1028 >
1029 <button type="submit" class="repo-settings-danger-btn">
1030 Delete this repository
1031 </button>
1032 </form>
1033 </div>
1034 </section>
1035 </div>
79136bbClaude1036 </Layout>
1037 );
1038});
1039
1040// Save settings
febd4f0Claude1041repoSettings.post("/:owner/:repo/settings", requireAuth, requireRepoAccess("admin"), async (c) => {
79136bbClaude1042 const { owner: ownerName, repo: repoName } = c.req.param();
1043 const user = c.get("user")!;
1044 const body = await c.req.parseBody();
1045
1046 const [owner] = await db
1047 .select()
1048 .from(users)
1049 .where(eq(users.username, ownerName))
1050 .limit(1);
1051
1052 if (!owner || owner.id !== user.id) {
1053 return c.redirect(`/${ownerName}/${repoName}`);
1054 }
1055
1056 await db
1057 .update(repositories)
1058 .set({
1059 description: String(body.description || "").trim() || null,
1060 defaultBranch: String(body.default_branch || "main"),
1061 isPrivate: body.visibility === "private",
1062 updatedAt: new Date(),
1063 })
1064 .where(
1065 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1066 );
1067
1068 return c.redirect(
1069 `/${ownerName}/${repoName}/settings?success=Settings+saved`
1070 );
1071});
1072
71cd5ecClaude1073// Toggle template flag
1074repoSettings.post(
1075 "/:owner/:repo/settings/template",
1076 requireAuth,
febd4f0Claude1077 requireRepoAccess("admin"),
71cd5ecClaude1078 async (c) => {
1079 const { owner: ownerName, repo: repoName } = c.req.param();
1080 const user = c.get("user")!;
1081 const body = await c.req.parseBody();
1082 const [owner] = await db
1083 .select()
1084 .from(users)
1085 .where(eq(users.username, ownerName))
1086 .limit(1);
1087 if (!owner || owner.id !== user.id) {
1088 return c.redirect(`/${ownerName}/${repoName}`);
1089 }
1090 const target = String(body.template || "1") === "1";
1091 await db
1092 .update(repositories)
1093 .set({ isTemplate: target, updatedAt: new Date() })
1094 .where(
1095 and(
1096 eq(repositories.ownerId, owner.id),
1097 eq(repositories.name, repoName)
1098 )
1099 );
1100 return c.redirect(
1101 `/${ownerName}/${repoName}/settings?success=${
1102 target ? "Marked+as+template" : "Unmarked+as+template"
1103 }`
1104 );
1105 }
1106);
1107
1108// Transfer repository to a new owner (by username)
1109repoSettings.post(
1110 "/:owner/:repo/settings/transfer",
1111 requireAuth,
febd4f0Claude1112 requireRepoAccess("admin"),
71cd5ecClaude1113 async (c) => {
1114 const { owner: ownerName, repo: repoName } = c.req.param();
1115 const user = c.get("user")!;
1116 const body = await c.req.parseBody();
1117 const newOwnerName = String(body.new_owner || "").trim();
1118 if (!newOwnerName) {
1119 return c.redirect(
1120 `/${ownerName}/${repoName}/settings?error=New+owner+required`
1121 );
1122 }
1123 const [owner] = await db
1124 .select()
1125 .from(users)
1126 .where(eq(users.username, ownerName))
1127 .limit(1);
1128 if (!owner || owner.id !== user.id) {
1129 return c.redirect(`/${ownerName}/${repoName}`);
1130 }
1131 const [newOwner] = await db
1132 .select()
1133 .from(users)
1134 .where(eq(users.username, newOwnerName))
1135 .limit(1);
1136 if (!newOwner) {
1137 return c.redirect(
1138 `/${ownerName}/${repoName}/settings?error=User+not+found`
1139 );
1140 }
1141 if (newOwner.id === owner.id) {
1142 return c.redirect(
1143 `/${ownerName}/${repoName}/settings?error=Same+owner`
1144 );
1145 }
1146 // Reject if new owner already has a repo by this name
1147 const [conflict] = await db
1148 .select()
1149 .from(repositories)
1150 .where(
1151 and(
1152 eq(repositories.ownerId, newOwner.id),
1153 eq(repositories.name, repoName)
1154 )
1155 )
1156 .limit(1);
1157 if (conflict) {
1158 return c.redirect(
1159 `/${ownerName}/${repoName}/settings?error=Target+owner+already+has+a+repo+by+that+name`
1160 );
1161 }
1162 const [repo] = await db
1163 .select()
1164 .from(repositories)
1165 .where(
1166 and(
1167 eq(repositories.ownerId, owner.id),
1168 eq(repositories.name, repoName)
1169 )
1170 )
1171 .limit(1);
1172 if (!repo) return c.notFound();
1173 await db
1174 .update(repositories)
1175 .set({ ownerId: newOwner.id, orgId: null, updatedAt: new Date() })
1176 .where(eq(repositories.id, repo.id));
1177 await db.insert(repoTransfers).values({
1178 repositoryId: repo.id,
1179 fromOwnerId: owner.id,
1180 fromOrgId: repo.orgId,
1181 toOwnerId: newOwner.id,
1182 toOrgId: null,
1183 initiatedBy: user.id,
1184 });
1185 return c.redirect(`/${newOwnerName}/${repoName}`);
1186 }
1187);
1188
1189// Archive / unarchive repository
1190repoSettings.post(
1191 "/:owner/:repo/settings/archive",
1192 requireAuth,
febd4f0Claude1193 requireRepoAccess("admin"),
71cd5ecClaude1194 async (c) => {
1195 const { owner: ownerName, repo: repoName } = c.req.param();
1196 const user = c.get("user")!;
1197 const body = await c.req.parseBody();
1198 const [owner] = await db
1199 .select()
1200 .from(users)
1201 .where(eq(users.username, ownerName))
1202 .limit(1);
1203 if (!owner || owner.id !== user.id) {
1204 return c.redirect(`/${ownerName}/${repoName}`);
1205 }
1206 const target = String(body.archive || "1") === "1";
1207 await db
1208 .update(repositories)
1209 .set({ isArchived: target, updatedAt: new Date() })
1210 .where(
1211 and(
1212 eq(repositories.ownerId, owner.id),
1213 eq(repositories.name, repoName)
1214 )
1215 );
1216 return c.redirect(
1217 `/${ownerName}/${repoName}/settings?success=${
1218 target ? "Repository+archived" : "Repository+unarchived"
1219 }`
1220 );
1221 }
1222);
1223
534f04aClaude1224// Block M5: stale activity opt-out flags. Owner-only; audits each toggle.
1225repoSettings.post(
1226 "/:owner/:repo/settings/stale",
1227 requireAuth,
1228 requireRepoAccess("admin"),
1229 async (c) => {
1230 const { owner: ownerName, repo: repoName } = c.req.param();
1231 const user = c.get("user")!;
1232 const body = await c.req.parseBody();
1233 const [owner] = await db
1234 .select()
1235 .from(users)
1236 .where(eq(users.username, ownerName))
1237 .limit(1);
1238 if (!owner || owner.id !== user.id) {
1239 return c.redirect(`/${ownerName}/${repoName}`);
1240 }
1241 const [repo] = await db
1242 .select()
1243 .from(repositories)
1244 .where(
1245 and(
1246 eq(repositories.ownerId, owner.id),
1247 eq(repositories.name, repoName)
1248 )
1249 )
1250 .limit(1);
1251 if (!repo) return c.notFound();
1252
1253 // Unchecked checkboxes are absent from the form payload, so coerce to bool.
1254 const newPrs = body.auto_close_stale_prs === "1";
1255 const newIssues = body.auto_close_stale_issues === "1";
1256
1257 await db
1258 .update(repositories)
1259 .set({
1260 autoCloseStalePrs: newPrs,
1261 autoCloseStaleIssues: newIssues,
1262 updatedAt: new Date(),
1263 })
1264 .where(eq(repositories.id, repo.id));
1265
1266 // Audit toggle deltas so the repo's audit log shows the change. Two
1267 // separate rows (one per flag) so the action names stay stable + grep-able.
1268 if (newPrs !== repo.autoCloseStalePrs) {
1269 await audit({
1270 userId: user.id,
1271 repositoryId: repo.id,
1272 action: "repo.auto_close_stale_prs.toggled",
1273 targetType: "repository",
1274 targetId: repo.id,
1275 metadata: { from: repo.autoCloseStalePrs, to: newPrs },
1276 });
1277 }
1278 if (newIssues !== repo.autoCloseStaleIssues) {
1279 await audit({
1280 userId: user.id,
1281 repositoryId: repo.id,
1282 action: "repo.auto_close_stale_issues.toggled",
1283 targetType: "repository",
1284 targetId: repo.id,
1285 metadata: { from: repo.autoCloseStaleIssues, to: newIssues },
1286 });
1287 }
1288
1289 return c.redirect(
1290 `/${ownerName}/${repoName}/settings?success=Stale+settings+saved`
1291 );
1292 }
1293);
1294
1d4ff60Claude1295// AI test generator opt-in. Owner-only; audits the toggle delta so the
1296// repo's audit log shows the change.
1297repoSettings.post(
1298 "/:owner/:repo/settings/ai-tests",
1299 requireAuth,
1300 requireRepoAccess("admin"),
1301 async (c) => {
1302 const { owner: ownerName, repo: repoName } = c.req.param();
1303 const user = c.get("user")!;
1304 const body = await c.req.parseBody();
1305 const [owner] = await db
1306 .select()
1307 .from(users)
1308 .where(eq(users.username, ownerName))
1309 .limit(1);
1310 if (!owner || owner.id !== user.id) {
1311 return c.redirect(`/${ownerName}/${repoName}`);
1312 }
1313 const [repo] = await db
1314 .select()
1315 .from(repositories)
1316 .where(
1317 and(
1318 eq(repositories.ownerId, owner.id),
1319 eq(repositories.name, repoName)
1320 )
1321 )
1322 .limit(1);
1323 if (!repo) return c.notFound();
1324
1325 // Unchecked checkboxes are absent from the form payload, so coerce.
1326 const next = body.auto_generate_tests === "1";
1327
1328 await db
1329 .update(repositories)
1330 .set({
1331 autoGenerateTests: next,
1332 updatedAt: new Date(),
1333 })
1334 .where(eq(repositories.id, repo.id));
1335
1336 if (next !== repo.autoGenerateTests) {
1337 await audit({
1338 userId: user.id,
1339 repositoryId: repo.id,
1340 action: "repo.auto_generate_tests.toggled",
1341 targetType: "repository",
1342 targetId: repo.id,
1343 metadata: { from: repo.autoGenerateTests, to: next },
1344 });
1345 }
1346
1347 return c.redirect(
1348 `/${ownerName}/${repoName}/settings?success=AI+test+settings+saved`
1349 );
1350 }
1351);
1352
9b3a183Claude1353// Migration 0072 — toggle cloud dev environments. Owner-only; audits
1354// the toggle delta so the repo's audit log shows the change.
1355repoSettings.post(
1356 "/:owner/:repo/settings/dev-envs",
1357 requireAuth,
1358 requireRepoAccess("admin"),
1359 async (c) => {
1360 const { owner: ownerName, repo: repoName } = c.req.param();
1361 const user = c.get("user")!;
1362 const body = await c.req.parseBody();
1363 const [owner] = await db
1364 .select()
1365 .from(users)
1366 .where(eq(users.username, ownerName))
1367 .limit(1);
1368 if (!owner || owner.id !== user.id) {
1369 return c.redirect(`/${ownerName}/${repoName}`);
1370 }
1371 const [repo] = await db
1372 .select()
1373 .from(repositories)
1374 .where(
1375 and(
1376 eq(repositories.ownerId, owner.id),
1377 eq(repositories.name, repoName)
1378 )
1379 )
1380 .limit(1);
1381 if (!repo) return c.notFound();
1382
1383 const next = body.dev_envs_enabled === "1";
1384 const prev = (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ?? false;
1385
1386 await db
1387 .update(repositories)
1388 .set({
1389 devEnvsEnabled: next,
1390 updatedAt: new Date(),
1391 })
1392 .where(eq(repositories.id, repo.id));
1393
1394 if (next !== prev) {
1395 await audit({
1396 userId: user.id,
1397 repositoryId: repo.id,
1398 action: "repo.dev_envs_enabled.toggled",
1399 targetType: "repository",
1400 targetId: repo.id,
1401 metadata: { from: prev, to: next },
1402 });
1403 }
1404
1405 return c.redirect(
1406 `/${ownerName}/${repoName}/settings?success=Dev+env+settings+saved#dev-envs`
1407 );
1408 }
1409);
1410
79136bbClaude1411// Delete repository
1412repoSettings.post(
1413 "/:owner/:repo/settings/delete",
1414 requireAuth,
febd4f0Claude1415 requireRepoAccess("admin"),
79136bbClaude1416 async (c) => {
1417 const { owner: ownerName, repo: repoName } = c.req.param();
1418 const user = c.get("user")!;
1419
1420 const [owner] = await db
1421 .select()
1422 .from(users)
1423 .where(eq(users.username, ownerName))
1424 .limit(1);
1425
1426 if (!owner || owner.id !== user.id) {
1427 return c.redirect(`/${ownerName}/${repoName}`);
1428 }
1429
1430 const [repo] = await db
1431 .select()
1432 .from(repositories)
1433 .where(
1434 and(
1435 eq(repositories.ownerId, owner.id),
1436 eq(repositories.name, repoName)
1437 )
1438 )
1439 .limit(1);
1440
1441 if (!repo) return c.redirect(`/${ownerName}`);
1442
1443 // Delete from disk
1444 try {
1445 await rm(repo.diskPath, { recursive: true, force: true });
1446 } catch {
1447 // Disk cleanup best-effort
1448 }
1449
1450 // Delete from DB (cascades to stars, issues, etc.)
1451 await db.delete(repositories).where(eq(repositories.id, repo.id));
1452
1453 return c.redirect(`/${ownerName}`);
1454 }
1455);
1456
1457export default repoSettings;