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.tsxBlame1476 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 = `
eed4684Claude27 .repo-settings-container { max-width: 1200px; margin: 0 auto; padding: 0 var(--space-3); }
58307aeClaude28
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>
44f1a02Claude696 <div class="repo-settings-field">
697 <label class="repo-settings-field-label">Data region</label>
698 <div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
699 <span
700 style={`display:inline-flex; align-items:center; gap:6px; padding:4px 12px; border-radius:9999px; font-size:12px; font-weight:600; font-family:var(--font-mono); ${
701 repo.dataRegion === "eu"
702 ? "background:rgba(54,197,214,0.10); color:#67e8f9; border:1px solid rgba(54,197,214,0.28);"
703 : "background:rgba(140,109,255,0.10); color:#c4b5fd; border:1px solid rgba(140,109,255,0.28);"
704 }`}
705 >
706 {repo.dataRegion === "eu" ? "EU · Frankfurt" : "US · Default"}
707 </span>
708 <span class="repo-settings-field-hint" style="margin:0;">
709 Data region is set at creation and cannot be changed. EU data
710 residency requires a{" "}
711 <a href="/pricing" style="color:var(--accent);text-decoration:none;">Pro plan or higher</a>.
712 </span>
713 </div>
714 </div>
79136bbClaude715 </div>
58307aeClaude716 <div class="repo-settings-section-foot">
717 <button type="submit" class="repo-settings-cta">
718 Save changes <span class="arrow">→</span>
719 </button>
720 </div>
721 </form>
722 </section>
14c3cc8Claude723
58307aeClaude724 {/* ─── Spec to PR ─── */}
725 <section class="repo-settings-section">
726 <div class="repo-settings-section-head">
727 <div class="repo-settings-section-eyebrow">
728 Automation · experimental
729 </div>
730 <h2 class="repo-settings-section-title">Spec to PR</h2>
731 <p class="repo-settings-section-desc">
732 Paste a plain-English feature spec and let Claude draft a pull
733 request for you. PRs open as drafts — review every line before
734 merging.
735 </p>
736 </div>
737 <div class="repo-settings-section-body">
738 <a
739 href={`/${ownerName}/${repoName}/spec`}
740 class="repo-settings-cta"
741 >
742 Open Spec to PR <span class="arrow">→</span>
743 </a>
744 </div>
745 </section>
746
747 {/* ─── Template repository ─── */}
748 <section class="repo-settings-section">
749 <div class="repo-settings-section-head">
750 <div class="repo-settings-section-eyebrow">Template</div>
751 <h2 class="repo-settings-section-title">Template repository</h2>
752 <p class="repo-settings-section-desc">
71cd5ecClaude753 {repo.isTemplate
58307aeClaude754 ? "This repository is a template. Users can click “Use this template” to create a new repository with the same files."
755 : "Mark this repository as a template so others can seed new repositories from its files."}
756 </p>
757 </div>
758 <div class="repo-settings-section-body">
759 <span
760 class={`repo-settings-pill ${repo.isTemplate ? "is-on" : "is-off"}`}
761 >
762 <span class="dot" aria-hidden="true" />
763 {repo.isTemplate ? "Template enabled" : "Not a template"}
764 </span>
765 <form
766 method="post"
767 action={`/${ownerName}/${repoName}/settings/template`}
768 style="margin-top: var(--space-3)"
769 >
770 <input
771 type="hidden"
772 name="template"
773 value={repo.isTemplate ? "0" : "1"}
774 />
775 <button type="submit" class="repo-settings-cta-secondary">
776 {repo.isTemplate ? "Unmark as template" : "Mark as template"}
777 </button>
778 </form>
779 </div>
780 </section>
71cd5ecClaude781
58307aeClaude782 {/* ─── Transfer ownership ─── */}
783 <section class="repo-settings-section">
784 <div class="repo-settings-section-head">
785 <div class="repo-settings-section-eyebrow">Ownership</div>
786 <h2 class="repo-settings-section-title">Transfer ownership</h2>
787 <p class="repo-settings-section-desc">
788 Hand this repository to another user. The new owner can accept
789 or decline the transfer by viewing it.
790 </p>
791 </div>
71cd5ecClaude792 <form
e7e240eClaude793 method="post"
71cd5ecClaude794 action={`/${ownerName}/${repoName}/settings/transfer`}
795 onsubmit="return confirm('Transfer this repository? The new owner will have full control.')"
796 >
58307aeClaude797 <div class="repo-settings-section-body">
798 <div class="repo-settings-field">
799 <label class="repo-settings-field-label" for="new_owner">
800 New owner username
801 </label>
802 <div class="repo-settings-inline-row">
803 <input
804 type="text"
805 name="new_owner"
806 id="new_owner"
807 class="repo-settings-input"
808 placeholder="new-owner-username"
809 required
810 aria-label="New owner username"
811 />
812 <button type="submit" class="repo-settings-cta-secondary">
813 Transfer
814 </button>
815 </div>
816 <div class="repo-settings-field-hint">
817 Transfers are immediate. The new owner can rename or delete
818 the repository at any time.
819 </div>
820 </div>
821 </div>
71cd5ecClaude822 </form>
58307aeClaude823 </section>
71cd5ecClaude824
58307aeClaude825 {/* ─── Archive ─── */}
826 <section class="repo-settings-section">
827 <div class="repo-settings-section-head">
828 <div class="repo-settings-section-eyebrow">Lifecycle</div>
829 <h2 class="repo-settings-section-title">
830 {repo.isArchived ? "Unarchive repository" : "Archive repository"}
831 </h2>
832 <p class="repo-settings-section-desc">
833 {repo.isArchived
834 ? "This repository is archived and read-only. Unarchive to allow pushes and issue/PR activity again."
835 : "Mark this repository as archived. It becomes read-only — no pushes, no new issues or PRs. You can unarchive at any time."}
836 </p>
837 </div>
838 <div class="repo-settings-section-body">
839 <span
840 class={`repo-settings-pill ${repo.isArchived ? "is-on" : "is-off"}`}
534f04aClaude841 >
58307aeClaude842 <span class="dot" aria-hidden="true" />
843 {repo.isArchived ? "Archived" : "Active"}
844 </span>
845 <form
846 method="post"
847 action={`/${ownerName}/${repoName}/settings/archive`}
848 style="margin-top: var(--space-3)"
534f04aClaude849 >
850 <input
58307aeClaude851 type="hidden"
852 name="archive"
853 value={repo.isArchived ? "0" : "1"}
534f04aClaude854 />
58307aeClaude855 <button type="submit" class="repo-settings-cta-secondary">
856 {repo.isArchived ? "Unarchive" : "Archive"} this repository
857 </button>
858 </form>
859 </div>
860 </section>
534f04aClaude861
1d4ff60Claude862 {/* ─── AI test generator ─── */}
863 <section class="repo-settings-section">
864 <div class="repo-settings-section-head">
865 <div class="repo-settings-section-eyebrow">AI tests</div>
866 <h2 class="repo-settings-section-title">Auto-generate tests on PR open</h2>
867 <p class="repo-settings-section-desc">
868 When a pull request opens, Gluecron AI reads the diff and writes
869 tests for the new code, matching whatever framework your repo
870 already uses. Tests land on the same branch. Default off —
871 opt in here.
872 </p>
873 </div>
874 <form
875 method="post"
876 action={`/${ownerName}/${repoName}/settings/ai-tests`}
877 >
878 <div class="repo-settings-section-body">
879 <label
880 class="repo-settings-toggle-row"
881 aria-label="Auto-generate tests when a PR opens"
882 >
883 <input
884 type="checkbox"
885 name="auto_generate_tests"
886 value="1"
887 checked={repo.autoGenerateTests}
888 />
889 <span class="repo-settings-toggle-text">
890 <span class="repo-settings-toggle-text-title">
891 Auto-generate tests on PR open
892 </span>
893 <span class="repo-settings-toggle-text-hint">
894 Requires <code>ANTHROPIC_API_KEY</code>. Skips PRs without
895 source-file changes and PRs already opened by AI.
896 </span>
897 </span>
898 </label>
899 </div>
900 <div class="repo-settings-section-foot">
901 <button type="submit" class="repo-settings-cta">
902 Save AI test settings <span class="arrow">→</span>
903 </button>
904 </div>
905 </form>
906 </section>
907
9b3a183Claude908 {/* ─── Cloud dev environments ─── */}
909 <section
910 id="dev-envs"
911 class="repo-settings-section"
912 >
913 <div class="repo-settings-section-head">
914 <div class="repo-settings-section-eyebrow">Dev environments</div>
915 <h2 class="repo-settings-section-title">Enable cloud dev environments</h2>
916 <p class="repo-settings-section-desc">
917 When enabled, anyone with read access can hit{" "}
918 <code>/{ownerName}/{repoName}/dev</code> to spin up a
919 hosted VS Code IDE in the browser, backed by a cold-start
920 container. We read <code>.gluecron/dev.yml</code> for the
921 image + install commands; idle envs stop themselves after
922 30 minutes. Default off — each env burns a container.
923 </p>
924 </div>
925 <form
926 method="post"
927 action={`/${ownerName}/${repoName}/settings/dev-envs`}
928 >
929 <div class="repo-settings-section-body">
930 <label
931 class="repo-settings-toggle-row"
932 aria-label="Enable cloud dev environments for this repo"
933 >
934 <input
935 type="checkbox"
936 name="dev_envs_enabled"
937 value="1"
938 checked={
939 (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ??
940 false
941 }
942 />
943 <span class="repo-settings-toggle-text">
944 <span class="repo-settings-toggle-text-title">
945 Enable dev environments
946 </span>
947 <span class="repo-settings-toggle-text-hint">
948 Surfaces the <code>/{ownerName}/{repoName}/dev</code>{" "}
949 route. Commit <code>.gluecron/dev.yml</code> to your
950 repo to customise the image, ports, and extensions.
951 </span>
952 </span>
953 </label>
954 </div>
955 <div class="repo-settings-section-foot">
956 <button type="submit" class="repo-settings-cta">
957 Save dev env settings <span class="arrow">→</span>
958 </button>
959 </div>
960 </form>
961 </section>
962
58307aeClaude963 {/* ─── Stale activity ─── */}
964 <section class="repo-settings-section">
965 <div class="repo-settings-section-head">
966 <div class="repo-settings-section-eyebrow">Stale sweep</div>
967 <h2 class="repo-settings-section-title">Stale activity</h2>
968 <p class="repo-settings-section-desc">
969 Autopilot pokes PRs and issues that have gone quiet, then offers
970 a one-click close path. Each toggle controls the final close
971 step — pokes always happen, but they&apos;re harmless reminders.
972 </p>
973 </div>
79136bbClaude974 <form
e7e240eClaude975 method="post"
58307aeClaude976 action={`/${ownerName}/${repoName}/settings/stale`}
79136bbClaude977 >
58307aeClaude978 <div class="repo-settings-section-body">
979 <label
980 class="repo-settings-toggle-row"
981 aria-label="Auto-close stale PRs after 14 days of no activity post-poke"
982 >
983 <input
984 type="checkbox"
985 name="auto_close_stale_prs"
986 value="1"
987 checked={repo.autoCloseStalePrs}
988 />
989 <span class="repo-settings-toggle-text">
990 <span class="repo-settings-toggle-text-title">
991 Auto-close stale PRs
992 </span>
993 <span class="repo-settings-toggle-text-hint">
994 Close PRs that go quiet for 14 days after autopilot pokes
995 them.
996 </span>
997 </span>
998 </label>
999 <label
1000 class="repo-settings-toggle-row"
1001 aria-label="Auto-close stale issues after 60 days of no activity post-poke"
1002 >
1003 <input
1004 type="checkbox"
1005 name="auto_close_stale_issues"
1006 value="1"
1007 checked={repo.autoCloseStaleIssues}
1008 />
1009 <span class="repo-settings-toggle-text">
1010 <span class="repo-settings-toggle-text-title">
1011 Auto-close stale issues
1012 </span>
1013 <span class="repo-settings-toggle-text-hint">
1014 Close issues that go quiet for 60 days after autopilot
1015 pokes them.
1016 </span>
1017 </span>
1018 </label>
1019 </div>
1020 <div class="repo-settings-section-foot">
1021 <button type="submit" class="repo-settings-cta">
1022 Save stale settings <span class="arrow">→</span>
1023 </button>
1024 </div>
79136bbClaude1025 </form>
58307aeClaude1026 </section>
1027
1028 {/* ─── Danger zone ─── */}
1029 <section class="repo-settings-danger">
1030 <div class="repo-settings-danger-head">
1031 <div class="repo-settings-danger-eyebrow">Danger zone</div>
1032 <h2 class="repo-settings-danger-title">Delete this repository</h2>
1033 <p class="repo-settings-danger-desc">
1034 Permanently remove this repository and every byte of its history,
1035 issues, PRs, stars, and webhooks. There is no undo.
1036 </p>
1037 </div>
1038 <div class="repo-settings-danger-body">
1039 <p class="muted">
1040 Once deleted, the URL <code>{ownerName}/{repoName}</code> frees
1041 up immediately. Open clones will fail to fetch or push.
1042 </p>
1043 <form
1044 method="post"
1045 action={`/${ownerName}/${repoName}/settings/delete`}
1046 onsubmit="return confirm('Are you sure? This cannot be undone.')"
1047 >
1048 <button type="submit" class="repo-settings-danger-btn">
1049 Delete this repository
1050 </button>
1051 </form>
1052 </div>
1053 </section>
1054 </div>
79136bbClaude1055 </Layout>
1056 );
1057});
1058
1059// Save settings
febd4f0Claude1060repoSettings.post("/:owner/:repo/settings", requireAuth, requireRepoAccess("admin"), async (c) => {
79136bbClaude1061 const { owner: ownerName, repo: repoName } = c.req.param();
1062 const user = c.get("user")!;
1063 const body = await c.req.parseBody();
1064
1065 const [owner] = await db
1066 .select()
1067 .from(users)
1068 .where(eq(users.username, ownerName))
1069 .limit(1);
1070
1071 if (!owner || owner.id !== user.id) {
1072 return c.redirect(`/${ownerName}/${repoName}`);
1073 }
1074
1075 await db
1076 .update(repositories)
1077 .set({
1078 description: String(body.description || "").trim() || null,
1079 defaultBranch: String(body.default_branch || "main"),
1080 isPrivate: body.visibility === "private",
1081 updatedAt: new Date(),
1082 })
1083 .where(
1084 and(eq(repositories.ownerId, owner.id), eq(repositories.name, repoName))
1085 );
1086
1087 return c.redirect(
1088 `/${ownerName}/${repoName}/settings?success=Settings+saved`
1089 );
1090});
1091
71cd5ecClaude1092// Toggle template flag
1093repoSettings.post(
1094 "/:owner/:repo/settings/template",
1095 requireAuth,
febd4f0Claude1096 requireRepoAccess("admin"),
71cd5ecClaude1097 async (c) => {
1098 const { owner: ownerName, repo: repoName } = c.req.param();
1099 const user = c.get("user")!;
1100 const body = await c.req.parseBody();
1101 const [owner] = await db
1102 .select()
1103 .from(users)
1104 .where(eq(users.username, ownerName))
1105 .limit(1);
1106 if (!owner || owner.id !== user.id) {
1107 return c.redirect(`/${ownerName}/${repoName}`);
1108 }
1109 const target = String(body.template || "1") === "1";
1110 await db
1111 .update(repositories)
1112 .set({ isTemplate: target, updatedAt: new Date() })
1113 .where(
1114 and(
1115 eq(repositories.ownerId, owner.id),
1116 eq(repositories.name, repoName)
1117 )
1118 );
1119 return c.redirect(
1120 `/${ownerName}/${repoName}/settings?success=${
1121 target ? "Marked+as+template" : "Unmarked+as+template"
1122 }`
1123 );
1124 }
1125);
1126
1127// Transfer repository to a new owner (by username)
1128repoSettings.post(
1129 "/:owner/:repo/settings/transfer",
1130 requireAuth,
febd4f0Claude1131 requireRepoAccess("admin"),
71cd5ecClaude1132 async (c) => {
1133 const { owner: ownerName, repo: repoName } = c.req.param();
1134 const user = c.get("user")!;
1135 const body = await c.req.parseBody();
1136 const newOwnerName = String(body.new_owner || "").trim();
1137 if (!newOwnerName) {
1138 return c.redirect(
1139 `/${ownerName}/${repoName}/settings?error=New+owner+required`
1140 );
1141 }
1142 const [owner] = await db
1143 .select()
1144 .from(users)
1145 .where(eq(users.username, ownerName))
1146 .limit(1);
1147 if (!owner || owner.id !== user.id) {
1148 return c.redirect(`/${ownerName}/${repoName}`);
1149 }
1150 const [newOwner] = await db
1151 .select()
1152 .from(users)
1153 .where(eq(users.username, newOwnerName))
1154 .limit(1);
1155 if (!newOwner) {
1156 return c.redirect(
1157 `/${ownerName}/${repoName}/settings?error=User+not+found`
1158 );
1159 }
1160 if (newOwner.id === owner.id) {
1161 return c.redirect(
1162 `/${ownerName}/${repoName}/settings?error=Same+owner`
1163 );
1164 }
1165 // Reject if new owner already has a repo by this name
1166 const [conflict] = await db
1167 .select()
1168 .from(repositories)
1169 .where(
1170 and(
1171 eq(repositories.ownerId, newOwner.id),
1172 eq(repositories.name, repoName)
1173 )
1174 )
1175 .limit(1);
1176 if (conflict) {
1177 return c.redirect(
1178 `/${ownerName}/${repoName}/settings?error=Target+owner+already+has+a+repo+by+that+name`
1179 );
1180 }
1181 const [repo] = await db
1182 .select()
1183 .from(repositories)
1184 .where(
1185 and(
1186 eq(repositories.ownerId, owner.id),
1187 eq(repositories.name, repoName)
1188 )
1189 )
1190 .limit(1);
1191 if (!repo) return c.notFound();
1192 await db
1193 .update(repositories)
1194 .set({ ownerId: newOwner.id, orgId: null, updatedAt: new Date() })
1195 .where(eq(repositories.id, repo.id));
1196 await db.insert(repoTransfers).values({
1197 repositoryId: repo.id,
1198 fromOwnerId: owner.id,
1199 fromOrgId: repo.orgId,
1200 toOwnerId: newOwner.id,
1201 toOrgId: null,
1202 initiatedBy: user.id,
1203 });
1204 return c.redirect(`/${newOwnerName}/${repoName}`);
1205 }
1206);
1207
1208// Archive / unarchive repository
1209repoSettings.post(
1210 "/:owner/:repo/settings/archive",
1211 requireAuth,
febd4f0Claude1212 requireRepoAccess("admin"),
71cd5ecClaude1213 async (c) => {
1214 const { owner: ownerName, repo: repoName } = c.req.param();
1215 const user = c.get("user")!;
1216 const body = await c.req.parseBody();
1217 const [owner] = await db
1218 .select()
1219 .from(users)
1220 .where(eq(users.username, ownerName))
1221 .limit(1);
1222 if (!owner || owner.id !== user.id) {
1223 return c.redirect(`/${ownerName}/${repoName}`);
1224 }
1225 const target = String(body.archive || "1") === "1";
1226 await db
1227 .update(repositories)
1228 .set({ isArchived: target, updatedAt: new Date() })
1229 .where(
1230 and(
1231 eq(repositories.ownerId, owner.id),
1232 eq(repositories.name, repoName)
1233 )
1234 );
1235 return c.redirect(
1236 `/${ownerName}/${repoName}/settings?success=${
1237 target ? "Repository+archived" : "Repository+unarchived"
1238 }`
1239 );
1240 }
1241);
1242
534f04aClaude1243// Block M5: stale activity opt-out flags. Owner-only; audits each toggle.
1244repoSettings.post(
1245 "/:owner/:repo/settings/stale",
1246 requireAuth,
1247 requireRepoAccess("admin"),
1248 async (c) => {
1249 const { owner: ownerName, repo: repoName } = c.req.param();
1250 const user = c.get("user")!;
1251 const body = await c.req.parseBody();
1252 const [owner] = await db
1253 .select()
1254 .from(users)
1255 .where(eq(users.username, ownerName))
1256 .limit(1);
1257 if (!owner || owner.id !== user.id) {
1258 return c.redirect(`/${ownerName}/${repoName}`);
1259 }
1260 const [repo] = await db
1261 .select()
1262 .from(repositories)
1263 .where(
1264 and(
1265 eq(repositories.ownerId, owner.id),
1266 eq(repositories.name, repoName)
1267 )
1268 )
1269 .limit(1);
1270 if (!repo) return c.notFound();
1271
1272 // Unchecked checkboxes are absent from the form payload, so coerce to bool.
1273 const newPrs = body.auto_close_stale_prs === "1";
1274 const newIssues = body.auto_close_stale_issues === "1";
1275
1276 await db
1277 .update(repositories)
1278 .set({
1279 autoCloseStalePrs: newPrs,
1280 autoCloseStaleIssues: newIssues,
1281 updatedAt: new Date(),
1282 })
1283 .where(eq(repositories.id, repo.id));
1284
1285 // Audit toggle deltas so the repo's audit log shows the change. Two
1286 // separate rows (one per flag) so the action names stay stable + grep-able.
1287 if (newPrs !== repo.autoCloseStalePrs) {
1288 await audit({
1289 userId: user.id,
1290 repositoryId: repo.id,
1291 action: "repo.auto_close_stale_prs.toggled",
1292 targetType: "repository",
1293 targetId: repo.id,
1294 metadata: { from: repo.autoCloseStalePrs, to: newPrs },
1295 });
1296 }
1297 if (newIssues !== repo.autoCloseStaleIssues) {
1298 await audit({
1299 userId: user.id,
1300 repositoryId: repo.id,
1301 action: "repo.auto_close_stale_issues.toggled",
1302 targetType: "repository",
1303 targetId: repo.id,
1304 metadata: { from: repo.autoCloseStaleIssues, to: newIssues },
1305 });
1306 }
1307
1308 return c.redirect(
1309 `/${ownerName}/${repoName}/settings?success=Stale+settings+saved`
1310 );
1311 }
1312);
1313
1d4ff60Claude1314// AI test generator opt-in. Owner-only; audits the toggle delta so the
1315// repo's audit log shows the change.
1316repoSettings.post(
1317 "/:owner/:repo/settings/ai-tests",
1318 requireAuth,
1319 requireRepoAccess("admin"),
1320 async (c) => {
1321 const { owner: ownerName, repo: repoName } = c.req.param();
1322 const user = c.get("user")!;
1323 const body = await c.req.parseBody();
1324 const [owner] = await db
1325 .select()
1326 .from(users)
1327 .where(eq(users.username, ownerName))
1328 .limit(1);
1329 if (!owner || owner.id !== user.id) {
1330 return c.redirect(`/${ownerName}/${repoName}`);
1331 }
1332 const [repo] = await db
1333 .select()
1334 .from(repositories)
1335 .where(
1336 and(
1337 eq(repositories.ownerId, owner.id),
1338 eq(repositories.name, repoName)
1339 )
1340 )
1341 .limit(1);
1342 if (!repo) return c.notFound();
1343
1344 // Unchecked checkboxes are absent from the form payload, so coerce.
1345 const next = body.auto_generate_tests === "1";
1346
1347 await db
1348 .update(repositories)
1349 .set({
1350 autoGenerateTests: next,
1351 updatedAt: new Date(),
1352 })
1353 .where(eq(repositories.id, repo.id));
1354
1355 if (next !== repo.autoGenerateTests) {
1356 await audit({
1357 userId: user.id,
1358 repositoryId: repo.id,
1359 action: "repo.auto_generate_tests.toggled",
1360 targetType: "repository",
1361 targetId: repo.id,
1362 metadata: { from: repo.autoGenerateTests, to: next },
1363 });
1364 }
1365
1366 return c.redirect(
1367 `/${ownerName}/${repoName}/settings?success=AI+test+settings+saved`
1368 );
1369 }
1370);
1371
9b3a183Claude1372// Migration 0072 — toggle cloud dev environments. Owner-only; audits
1373// the toggle delta so the repo's audit log shows the change.
1374repoSettings.post(
1375 "/:owner/:repo/settings/dev-envs",
1376 requireAuth,
1377 requireRepoAccess("admin"),
1378 async (c) => {
1379 const { owner: ownerName, repo: repoName } = c.req.param();
1380 const user = c.get("user")!;
1381 const body = await c.req.parseBody();
1382 const [owner] = await db
1383 .select()
1384 .from(users)
1385 .where(eq(users.username, ownerName))
1386 .limit(1);
1387 if (!owner || owner.id !== user.id) {
1388 return c.redirect(`/${ownerName}/${repoName}`);
1389 }
1390 const [repo] = await db
1391 .select()
1392 .from(repositories)
1393 .where(
1394 and(
1395 eq(repositories.ownerId, owner.id),
1396 eq(repositories.name, repoName)
1397 )
1398 )
1399 .limit(1);
1400 if (!repo) return c.notFound();
1401
1402 const next = body.dev_envs_enabled === "1";
1403 const prev = (repo as { devEnvsEnabled?: boolean }).devEnvsEnabled ?? false;
1404
1405 await db
1406 .update(repositories)
1407 .set({
1408 devEnvsEnabled: next,
1409 updatedAt: new Date(),
1410 })
1411 .where(eq(repositories.id, repo.id));
1412
1413 if (next !== prev) {
1414 await audit({
1415 userId: user.id,
1416 repositoryId: repo.id,
1417 action: "repo.dev_envs_enabled.toggled",
1418 targetType: "repository",
1419 targetId: repo.id,
1420 metadata: { from: prev, to: next },
1421 });
1422 }
1423
1424 return c.redirect(
1425 `/${ownerName}/${repoName}/settings?success=Dev+env+settings+saved#dev-envs`
1426 );
1427 }
1428);
1429
79136bbClaude1430// Delete repository
1431repoSettings.post(
1432 "/:owner/:repo/settings/delete",
1433 requireAuth,
febd4f0Claude1434 requireRepoAccess("admin"),
79136bbClaude1435 async (c) => {
1436 const { owner: ownerName, repo: repoName } = c.req.param();
1437 const user = c.get("user")!;
1438
1439 const [owner] = await db
1440 .select()
1441 .from(users)
1442 .where(eq(users.username, ownerName))
1443 .limit(1);
1444
1445 if (!owner || owner.id !== user.id) {
1446 return c.redirect(`/${ownerName}/${repoName}`);
1447 }
1448
1449 const [repo] = await db
1450 .select()
1451 .from(repositories)
1452 .where(
1453 and(
1454 eq(repositories.ownerId, owner.id),
1455 eq(repositories.name, repoName)
1456 )
1457 )
1458 .limit(1);
1459
1460 if (!repo) return c.redirect(`/${ownerName}`);
1461
1462 // Delete from disk
1463 try {
1464 await rm(repo.diskPath, { recursive: true, force: true });
1465 } catch {
1466 // Disk cleanup best-effort
1467 }
1468
1469 // Delete from DB (cascades to stars, issues, etc.)
1470 await db.delete(repositories).where(eq(repositories.id, repo.id));
1471
1472 return c.redirect(`/${ownerName}`);
1473 }
1474);
1475
1476export default repoSettings;