g1t/apps/web/app/routes/workspace/package.tsx

364 lines15,818 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start1import { BookOpen, Box, Trash2 } from "lucide-react";
Merge branch 'worktree-agent-a985c01f43538af01'2import { Form, Link, data, redirect } from "react-router";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member3
4import { ECOSYSTEMS, type Ecosystem, type PackageVersion } from "@g1t/contracts";
5
6import type { Route } from "./+types/package";
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token7import { Markdown } from "../../components/markdown";
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start8import { PackageIcon } from "../../components/package-icon";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member9import { ConfirmDialog } from "../../components/repo-lifecycle";
Merge branch 'worktree-agent-a985c01f43538af01'10import { Button, CopyLine, ErrorText, SubmitButton, TimeAgo } from "../../components/ui";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member11import { Badge } from "../../components/ui/badge";
12import { page } from "../../lib/meta";
13import { ECOSYSTEM_LABEL, formatBytes, installCommands, shortDigest } from "../../lib/packages";
14import { packages } from "../../lib/services.server";
15import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
16
17function ecosystemOf(value: string | undefined): Ecosystem {
18 if (value && (ECOSYSTEMS as readonly string[]).includes(value)) return value as Ecosystem;
19 throw data(null, { status: 404 });
20}
21
22export function meta({ params, ...args }: Route.MetaArgs) {
23 return page(args, { title: `${params["*"]} · Packages · ${params.owner} · g1t` });
24}
25
26export async function loader({ params, context }: Route.LoaderArgs) {
27 const viewer = getViewer(context);
28 const ecosystem = ecosystemOf(params.ecosystem);
29 const name = params["*"] ?? "";
30 const found = await packages.get(params.owner, ecosystem, name, viewer);
31 // Not found and not allowed look the same.
32 if (!found.ok) throw data(null, { status: 404 });
33 return { detail: found.value, username: viewer?.username ?? "you" };
34}
35
36type Outcome = { error: string | null; message: string | null };
37
38export async function action({ request, params, context }: Route.ActionArgs): Promise<Outcome | Response> {
39 assertSameOrigin(request);
40 const user = requireUser(context, request);
41 const ecosystem = ecosystemOf(params.ecosystem);
42 const name = params["*"] ?? "";
43 const form = await request.formData();
44 const intent = String(form.get("intent") ?? "");
45 const fail = (message: string): Outcome => ({ error: message, message: null });
46 if (intent === "delete-version") {
47 const version = String(form.get("version") ?? "");
48 const done = await packages.deleteVersion(user, params.owner, ecosystem, name, version, "web");
49 return done.ok ? { error: null, message: `Deleted ${shortDigest(version)} and its tags.` } : fail(done.error.message);
50 }
51 if (intent === "delete-package") {
52 const done = await packages.deletePackage(user, params.owner, ecosystem, name, "web");
53 if (!done.ok) return fail(done.error.message);
54 return redirect(`/${params.owner}/-/packages`);
55 }
56 if (intent === "visibility") {
57 const visibility = form.get("visibility") === "public" ? "public" : "private";
58 const done = await packages.set(user, params.owner, ecosystem, name, { visibility }, "web");
59 return done.ok ? { error: null, message: `It is ${visibility} now.` } : fail(done.error.message);
60 }
61 if (intent === "link") {
62 const repo = String(form.get("repo") ?? "").trim();
63 const done = repo
64 ? await packages.set(user, params.owner, ecosystem, name, { link: repo }, "web")
65 : await packages.set(user, params.owner, ecosystem, name, { unlink: true }, "web");
66 return done.ok ? { error: null, message: repo ? `Linked to ${repo}.` : "Unlinked." } : fail(done.error.message);
67 }
68 return fail("That is not something this page does.");
69}
70
71export default function PackagePage({ loaderData, actionData }: Route.ComponentProps) {
72 const { detail, username } = loaderData;
73 const { package: pkg, versions, tags, permissions } = detail;
74 const outcome = actionData as Outcome | undefined;
Merge branch 'worktree-agent-a6a121745e81f639f'75 // Maven names a version to fetch; the others install their newest without one.
76 const latest = tags.find((tag) => tag.tag === "latest")?.tag ?? tags[0]?.tag ?? (pkg.ecosystem === "maven" ? pkg.latest : null);
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member77 const commands = installCommands(pkg, latest, username);
Composer from the workspace's own repositories, and go get from g1t.sh78 // npm and Composer versions are numbers; images are digests and tags.
79 const npm = pkg.ecosystem !== "container";
80 // Composer's versions are the repository's tags and branches: they
81 // change in git, not here.
82 const fromGit = pkg.ecosystem === "composer";
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member83 // Signatures and attestations hang off the images they describe.
84 const images = versions.filter((version) => !version.subject);
85 const attached = (digest: string) => versions.filter((version) => version.subject === digest);
86 return (
87 <div className="space-y-8">
88 <header className="space-y-3">
89 <Link to={`/${pkg.workspace}/-/packages`} className="text-sm text-muted hover:text-fg">
90 Packages
91 </Link>
92 <div className="flex flex-wrap items-center gap-3">
Packages lists like a registry: Type, search, Visibility and Sort in one bar, richer rows, and a registry to choose from to start93 <PackageIcon ecosystem={pkg.ecosystem} size={32} />
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member94 <h1 className="text-2xl font-semibold tracking-tight">{pkg.name}</h1>
95 <Badge>{ECOSYSTEM_LABEL[pkg.ecosystem]}</Badge>
96 <Badge>{pkg.visibility === "private" ? "Private" : "Public"}</Badge>
97 </div>
98 <p className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted tabular-nums">
99 {pkg.repo && (
100 <Link to={`/${pkg.repo.namespace}/${pkg.repo.name}`} className="inline-flex items-center gap-1 hover:text-fg">
101 <Box size={13} />
102 {pkg.repo.namespace}/{pkg.repo.name}
103 </Link>
104 )}
105 <span>{formatBytes(pkg.size)}</span>
106 <span>
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token107 {pkg.downloads.toLocaleString("en-US")}{" "}
108 {npm ? (pkg.downloads === 1 ? "download" : "downloads") : pkg.downloads === 1 ? "pull" : "pulls"}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member109 </span>
110 <span>
111 Updated <TimeAgo at={pkg.updated_at} />
112 </span>
113 </p>
114 {pkg.description && <p className="max-w-2xl text-sm text-fg-soft">{pkg.description}</p>}
115 </header>
116
117 {outcome?.error && <ErrorText>{outcome.error}</ErrorText>}
118 {outcome?.message && <p className="text-sm text-accent">{outcome.message}</p>}
119
120 <section className="space-y-2">
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token121 <h2 className="text-sm font-semibold">{npm ? "Install it" : "Pull it"}</h2>
122 {commands.registry && <CopyLine prompt text={commands.registry} />}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member123 {pkg.visibility === "private" && <CopyLine prompt text={commands.login} />}
124 <CopyLine prompt text={commands.install} />
Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version125 {commands.registry && pkg.visibility === "private" && pkg.ecosystem === "cargo" && (
126 <p className="text-xs text-faint">
127 <code className="font-mono">cargo login</code> asks for an{" "}
128 <Link to="/settings/tokens" className="text-muted hover:text-fg">
129 access token
130 </Link>{" "}
131 with <code className="font-mono">packages:read</code>.
132 </p>
133 )}
Merge branch 'worktree-agent-a6a121745e81f639f'134 {pkg.visibility === "private" && commands.login.includes("YOUR_TOKEN") && (
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token135 <p className="text-xs text-faint">
136 Put an{" "}
137 <Link to="/settings/tokens" className="text-muted hover:text-fg">
138 access token
139 </Link>{" "}
140 with <code className="font-mono">packages:read</code> in place of YOUR_TOKEN.
141 </p>
142 )}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member143 </section>
144
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token145 {detail.readme && (
146 <section className="overflow-hidden rounded-xl border border-line">
147 <h2 className="flex items-center gap-2 border-b border-line bg-surface px-4 py-2.5 text-sm font-medium">
148 <BookOpen size={15} className="text-faint" />
149 README
150 </h2>
151 <div className="p-6">
152 <Markdown
153 source={detail.readme}
154 repo={pkg.repo ? { namespace: pkg.repo.namespace, name: pkg.repo.name } : undefined}
155 />
156 </div>
157 </section>
158 )}
159
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member160 <section className="space-y-3">
161 <h2 className="text-sm font-semibold">
162 Versions <span className="font-normal text-faint">{images.length}</span>
163 </h2>
Composer from the workspace's own repositories, and go get from g1t.sh164 {fromGit && (
165 <p className="text-xs text-faint">
166 Each tag of {pkg.repo ? `${pkg.repo.namespace}/${pkg.repo.name}` : "its repository"} that reads as a version, and each
167 branch as <code className="font-mono">dev-</code>, from its composer.json. Push a tag to publish one; delete it to take it away.
168 </p>
169 )}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member170 {images.length === 0 ? (
171 <p className="text-sm text-muted">No versions are left.</p>
172 ) : (
173 <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
174 {images.map((version) => (
Composer from the workspace's own repositories, and go get from g1t.sh175 <VersionRow
176 key={version.id}
177 version={version}
178 attached={attached(version.digest)}
179 canDelete={permissions.delete && !fromGit}
180 npm={npm}
181 />
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member182 ))}
183 </ul>
184 )}
185 </section>
186
187 {permissions.admin && <Settings detail={detail} />}
188 </div>
189 );
190}
191
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token192function VersionRow({
193 version,
194 attached,
195 canDelete,
196 npm,
197}: {
198 version: PackageVersion;
199 attached: PackageVersion[];
200 canDelete: boolean;
201 npm: boolean;
202}) {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member203 return (
204 <li className="flex flex-wrap items-start gap-x-4 gap-y-2 px-4 py-3">
205 <div className="min-w-0 grow space-y-1">
206 <div className="flex flex-wrap items-center gap-1.5">
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token207 {npm && <span className="font-mono text-sm font-medium">{version.version}</span>}
208 {npm ? (
209 version.tags.map((tag) => (
210 <Badge key={tag} tone={tag === "latest" ? "accent" : "neutral"} className="font-mono">
211 {tag}
212 </Badge>
213 ))
214 ) : version.tags.length > 0 ? (
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member215 version.tags.map((tag) => (
216 <Badge key={tag} tone={tag === "latest" ? "accent" : "neutral"} className="font-mono">
217 {tag}
218 </Badge>
219 ))
220 ) : (
221 <span className="text-xs text-faint">Untagged</span>
222 )}
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token223 {!npm && (
224 <code className="font-mono text-xs text-muted" title={version.digest}>
225 {shortDigest(version.digest)}
226 </code>
227 )}
228 {version.deprecated && (
229 <Badge tone="neutral" title={version.deprecated}>
230 Deprecated
231 </Badge>
232 )}
Merge branch 'worktree-agent-ac1de8a731938ed81'233 {version.symbols && (
234 <Badge tone="neutral" title="A symbol package (.snupkg) was pushed: debuggers load its PDBs from the feed's symbol server.">
235 Symbols
236 </Badge>
237 )}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member238 </div>
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token239 {version.deprecated && <p className="text-xs text-muted">{version.deprecated}</p>}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member240 <p className="flex flex-wrap gap-x-3 text-xs text-faint tabular-nums">
241 <span>{formatBytes(version.size)}</span>
Merge branch 'worktree-agent-ac1de8a731938ed81'242 {version.downloads != null && (
243 <span>
244 {version.downloads.toLocaleString("en-US")} {version.downloads === 1 ? "download" : "downloads"}
245 </span>
246 )}
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member247 {version.platforms.length > 0 && <span>{version.platforms.join(", ")}</span>}
248 {attached.length > 0 && (
249 <span title={attached.map((a) => a.artifact_type ?? a.media_type ?? "artifact").join(", ")}>
250 {attached.length} attached ({attached.map((a) => artifactWord(a)).join(", ")})
251 </span>
252 )}
253 <span>
254 {version.published_by ? `${version.published_by} · ` : ""}
255 <TimeAgo at={version.published_at} />
256 </span>
257 </p>
258 </div>
259 {canDelete && (
260 <ConfirmDialog
261 intent="delete-version"
npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token262 fields={{ version: npm ? version.version : version.digest }}
263 title={`Delete ${npm ? version.version : (version.tags[0] ?? shortDigest(version.digest))}?`}
264 description={
265 npm
266 ? "Anyone installing this version gets an error from then on."
267 : "Anyone pulling it by this tag or digest gets an error from then on."
268 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member269 submit="Delete version"
270 busy="Deleting…"
271 trigger={(open) => (
272 <button type="button" onClick={open} aria-label="Delete version" className="rounded-md p-1.5 text-faint hover:bg-raised hover:text-danger">
273 <Trash2 size={14} />
274 </button>
275 )}
276 >
277 <li>Its tags go with it.</li>
278 <li>Files no other version uses are removed within a day.</li>
279 </ConfirmDialog>
280 )}
281 </li>
282 );
283}
284
285/** "signature", "SBOM", "attestation", or what the artifact says it is. */
286function artifactWord(version: PackageVersion): string {
287 const type = version.artifact_type ?? version.media_type ?? "";
288 if (/signature|cosign|notary/i.test(type)) return "signature";
289 if (/spdx|cyclonedx|sbom/i.test(type)) return "SBOM";
290 if (/in-toto|attestation|provenance/i.test(type)) return "attestation";
291 return "artifact";
292}
293
294function Settings({ detail }: { detail: Route.ComponentProps["loaderData"]["detail"] }) {
295 const { package: pkg } = detail;
296 return (
297 <section className="space-y-4 rounded-xl border border-line bg-surface p-5">
298 <h2 className="text-sm font-semibold">Settings</h2>
299 {pkg.repo ? (
300 <p className="text-sm text-muted">
301 Linked to{" "}
302 <Link to={`/${pkg.repo.namespace}/${pkg.repo.name}`} className="text-fg-soft hover:text-fg">
303 {pkg.repo.namespace}/{pkg.repo.name}
304 </Link>
305 : it has the repository's visibility and roles.
306 </p>
307 ) : (
308 <Form method="post" className="flex flex-wrap items-center gap-2 text-sm">
309 <input type="hidden" name="intent" value="visibility" />
310 <span className="text-muted">Visibility</span>
311 <select
312 name="visibility"
313 defaultValue={pkg.visibility}
314 className="h-8 rounded-md border border-line bg-bg px-2 text-sm"
315 aria-label="Visibility"
316 >
317 <option value="private">Private: workspace members</option>
318 <option value="public">Public: anyone can pull</option>
319 </select>
Merge branch 'worktree-agent-a985c01f43538af01'320 <SubmitButton variant="quiet" match={{ intent: "visibility" }} pending="Saving…">
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member321 Save
Merge branch 'worktree-agent-a985c01f43538af01'322 </SubmitButton>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member323 </Form>
324 )}
325 <Form method="post" className="flex flex-wrap items-center gap-2 text-sm">
326 <input type="hidden" name="intent" value="link" />
327 <label className="text-muted" htmlFor="link-repo">
328 Repository
329 </label>
330 <input
331 id="link-repo"
332 name="repo"
333 defaultValue={pkg.repo?.name ?? ""}
334 placeholder="none"
335 className="h-8 w-56 rounded-md border border-line bg-bg px-2 font-mono text-sm placeholder:text-faint"
336 />
Merge branch 'worktree-agent-a985c01f43538af01'337 <SubmitButton variant="quiet" match={{ intent: "link" }} pending="Saving…">
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member338 {pkg.repo ? "Change link" : "Link"}
Merge branch 'worktree-agent-a985c01f43538af01'339 </SubmitButton>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member340 <span className="text-xs text-faint">Empty to unlink. A linked package takes the repository's access.</span>
341 </Form>
342 {detail.permissions.delete && (
343 <div className="border-t border-line pt-4">
344 <ConfirmDialog
345 intent="delete-package"
346 title={`Delete ${pkg.name}?`}
347 description="Every version and tag goes, and anyone pulling it gets an error."
348 confirm={pkg.name}
349 submit="Delete package"
350 busy="Deleting…"
351 trigger={(open) => (
352 <Button type="button" variant="danger" onClick={open}>
353 Delete package
354 </Button>
355 )}
356 >
357 <li>{detail.versions.length} versions and their tags.</li>
358 <li>The name can be pushed again afterwards.</li>
359 </ConfirmDialog>
360 </div>
361 )}
362 </section>
363 );
364}

This file's history is long; its oldest lines are credited to the oldest commit read.