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 start | 1 | import { BookOpen, Box, Trash2 } from "lucide-react"; |
| Merge packages: roles, Actions access, source label, soft delete, API | 2 | import { Link, data, redirect, useLocation, useNavigation } from "react-router"; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 3 | |
| Merge packages: roles, Actions access, source label, soft delete, API | 4 | import { ECOSYSTEMS, type Ecosystem, type PackageGrantee, type PackageRole, type PackageSettings, type PackageVersion, PACKAGE_RESTORE_DAYS } from "@g1t/contracts"; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 5 | |
| 6 | import type { Route } from "./+types/package"; | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 7 | import { 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 start | 8 | import { PackageIcon } from "../../components/package-icon"; |
| Merge packages: roles, Actions access, source label, soft delete, API | 9 | import { PackageSettingsSkeleton, PackageSettingsTab, type SettingsOutcome } from "../../components/package-settings"; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 10 | import { ConfirmDialog } from "../../components/repo-lifecycle"; |
| Merge packages: roles, Actions access, source label, soft delete, API | 11 | import { CopyLine, ErrorText, TimeAgo } from "../../components/ui"; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 12 | import { Badge } from "../../components/ui/badge"; |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 13 | import { Hint } from "../../components/ui/hint"; |
| Merge packages: roles, Actions access, source label, soft delete, API | 14 | import { Loading } from "../../components/ui/skeleton"; |
| 15 | import { TabStrip } from "../../components/ui/tab-strip"; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 16 | import { page } from "../../lib/meta"; |
| 17 | import { ECOSYSTEM_LABEL, formatBytes, installCommands, shortDigest } from "../../lib/packages"; | |
| 18 | import { packages } from "../../lib/services.server"; | |
| 19 | import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server"; | |
| 20 | ||
| 21 | function ecosystemOf(value: string | undefined): Ecosystem { | |
| 22 | if (value && (ECOSYSTEMS as readonly string[]).includes(value)) return value as Ecosystem; | |
| 23 | throw data(null, { status: 404 }); | |
| 24 | } | |
| 25 | ||
| Merge packages: roles, Actions access, source label, soft delete, API | 26 | type Tab = "overview" | "settings"; |
| 27 | ||
| 28 | function tabOf(search: string): Tab { | |
| 29 | return new URLSearchParams(search).get("tab") === "settings" ? "settings" : "overview"; | |
| 30 | } | |
| 31 | ||
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 32 | export function meta({ params, ...args }: Route.MetaArgs) { |
| 33 | return page(args, { title: `${params["*"]} · Packages · ${params.owner} · g1t` }); | |
| 34 | } | |
| 35 | ||
| Merge packages: roles, Actions access, source label, soft delete, API | 36 | export async function loader({ params, context, request }: Route.LoaderArgs) { |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 37 | const viewer = getViewer(context); |
| 38 | const ecosystem = ecosystemOf(params.ecosystem); | |
| 39 | const name = params["*"] ?? ""; | |
| Merge packages: roles, Actions access, source label, soft delete, API | 40 | const tab = tabOf(new URL(request.url).search); |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 41 | const found = await packages.get(params.owner, ecosystem, name, viewer); |
| 42 | // Not found and not allowed look the same. | |
| 43 | if (!found.ok) throw data(null, { status: 404 }); | |
| Merge packages: roles, Actions access, source label, soft delete, API | 44 | let settings: PackageSettings | null = null; |
| 45 | if (tab === "settings") { | |
| 46 | // Only the package's admins have a Settings tab. | |
| 47 | if (!found.value.permissions.admin) throw redirect(`/${params.owner}/-/packages/${ecosystem}/${name}`); | |
| 48 | const got = await packages.settings(params.owner, ecosystem, name, viewer); | |
| 49 | if (!got.ok) throw data(null, { status: 404 }); | |
| 50 | settings = got.value; | |
| 51 | } | |
| 52 | return { detail: found.value, username: viewer?.username ?? "you", tab, settings }; | |
| 53 | } | |
| 54 | ||
| 55 | /** Who an access change names: a person by username, or a team by slug. */ | |
| 56 | function grantee(form: FormData): PackageGrantee | null { | |
| 57 | const kind = String(form.get("kind") ?? ""); | |
| 58 | const user = String(form.get("user") ?? (kind === "user" ? form.get("who") : "") ?? "").trim(); | |
| 59 | const team = String(form.get("team") ?? (kind === "team" ? form.get("who") : "") ?? "").trim(); | |
| 60 | if (user) return { user }; | |
| 61 | if (team) return { team }; | |
| 62 | return null; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 63 | } |
| 64 | ||
| Merge packages: roles, Actions access, source label, soft delete, API | 65 | function roleOf(form: FormData): PackageRole | null { |
| 66 | const role = String(form.get("role") ?? ""); | |
| 67 | return role === "read" || role === "write" || role === "admin" ? role : null; | |
| 68 | } | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 69 | |
| Merge packages: roles, Actions access, source label, soft delete, API | 70 | export async function action({ request, params, context }: Route.ActionArgs): Promise<SettingsOutcome | Response> { |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 71 | assertSameOrigin(request); |
| 72 | const user = requireUser(context, request); | |
| 73 | const ecosystem = ecosystemOf(params.ecosystem); | |
| 74 | const name = params["*"] ?? ""; | |
| Merge packages: roles, Actions access, source label, soft delete, API | 75 | const ws = params.owner; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 76 | const form = await request.formData(); |
| 77 | const intent = String(form.get("intent") ?? ""); | |
| Merge packages: roles, Actions access, source label, soft delete, API | 78 | const fail = (message: string): SettingsOutcome => ({ error: message, message: null }); |
| 79 | const done = (result: { ok: true } | { ok: false; error: { message: string } }, message: string): SettingsOutcome => | |
| 80 | result.ok ? { error: null, message } : fail(result.error.message); | |
| 81 | switch (intent) { | |
| 82 | case "delete-version": { | |
| 83 | const version = String(form.get("version") ?? ""); | |
| 84 | return done( | |
| 85 | await packages.deleteVersion(user, ws, ecosystem, name, version, "web"), | |
| 86 | `Deleted ${shortDigest(version)} and its tags. An admin can restore it from Settings for ${PACKAGE_RESTORE_DAYS} days.`, | |
| 87 | ); | |
| 88 | } | |
| 89 | case "delete-package": { | |
| 90 | const deleted = await packages.deletePackage(user, ws, ecosystem, name, "web"); | |
| 91 | if (!deleted.ok) return fail(deleted.error.message); | |
| 92 | return redirect(`/${ws}/-/packages?view=deleted`); | |
| 93 | } | |
| 94 | case "restore-version": { | |
| 95 | const version = String(form.get("version") ?? ""); | |
| 96 | const restored = await packages.restoreVersion(user, ws, ecosystem, name, version, "web"); | |
| 97 | return done(restored, restored.ok ? `Restored ${shortDigest(restored.value.version)}.` : ""); | |
| 98 | } | |
| 99 | case "visibility": { | |
| 100 | const visibility = form.get("visibility") === "public" ? "public" : "private"; | |
| 101 | return done(await packages.set(user, ws, ecosystem, name, { visibility }, "web"), `It is ${visibility} now.`); | |
| 102 | } | |
| 103 | case "link": { | |
| 104 | const repo = String(form.get("repo") ?? "").trim(); | |
| 105 | if (!repo) return fail("Name a repository of the workspace to link it to."); | |
| 106 | return done(await packages.set(user, ws, ecosystem, name, { link: repo }, "web"), `Linked to ${repo}.`); | |
| 107 | } | |
| 108 | case "unlink": | |
| 109 | return done(await packages.set(user, ws, ecosystem, name, { unlink: true }, "web"), "Unlinked: it is the workspace's now."); | |
| 110 | case "inherit": { | |
| 111 | const on = form.get("inherit") === "on"; | |
| 112 | return done( | |
| 113 | await packages.set(user, ws, ecosystem, name, { inheritAccess: on }, "web"), | |
| 114 | on ? "It inherits access from its repository again." : "It no longer inherits access from its repository.", | |
| 115 | ); | |
| 116 | } | |
| 117 | case "access-add": | |
| 118 | case "access-role": { | |
| 119 | const who = grantee(form); | |
| 120 | const role = roleOf(form); | |
| 121 | if (!who) return fail("Name a person by username, or a team by its slug."); | |
| 122 | if (!role) return fail("Choose a role: read, write or admin."); | |
| 123 | const label = "user" in who ? who.user : `the team ${who.team}`; | |
| 124 | return done(await packages.setAccess(user, ws, ecosystem, name, who, role, "web"), `${label} has the ${role} role.`); | |
| 125 | } | |
| 126 | case "access-remove": { | |
| 127 | const who = grantee(form); | |
| 128 | if (!who) return fail("Name a person by username, or a team by its slug."); | |
| 129 | return done(await packages.removeAccess(user, ws, ecosystem, name, who, "web"), "Removed."); | |
| 130 | } | |
| 131 | case "actions-add": | |
| 132 | case "actions-role": { | |
| 133 | const repo = String(form.get("repo") ?? "").trim(); | |
| 134 | const role = form.get("role") === "write" ? "write" : "read"; | |
| 135 | if (!repo) return fail("Name a repository of the workspace."); | |
| 136 | return done( | |
| 137 | await packages.setActionsAccess(user, ws, ecosystem, name, repo, role, "web"), | |
| 138 | `Workflows in ${repo} may ${role === "write" ? "publish" : "pull"} it.`, | |
| 139 | ); | |
| 140 | } | |
| 141 | case "actions-remove": { | |
| 142 | const repo = String(form.get("repo") ?? "").trim(); | |
| 143 | return done(await packages.removeActionsAccess(user, ws, ecosystem, name, repo, "web"), `Workflows in ${repo} can no longer use it.`); | |
| 144 | } | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 145 | } |
| 146 | return fail("That is not something this page does."); | |
| 147 | } | |
| 148 | ||
| 149 | export default function PackagePage({ loaderData, actionData }: Route.ComponentProps) { | |
| Merge packages: roles, Actions access, source label, soft delete, API | 150 | const { detail, settings } = loaderData; |
| 151 | const { package: pkg, permissions } = detail; | |
| 152 | const outcome = actionData as SettingsOutcome | undefined; | |
| 153 | const navigation = useNavigation(); | |
| 154 | const { pathname } = useLocation(); | |
| 155 | // The tab being opened shows at once, its outline until its content | |
| 156 | // follows; a form posted on the page keeps the page as it is meanwhile. | |
| 157 | const switching = navigation.state === "loading" && !navigation.formMethod && navigation.location.pathname === pathname; | |
| 158 | const going = switching && tabOf(navigation.location.search) !== loaderData.tab ? tabOf(navigation.location.search) : null; | |
| 159 | const tab = going ?? loaderData.tab; | |
| 160 | const base = `/${pkg.workspace}/-/packages/${pkg.ecosystem}/${pkg.name}`; | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 161 | return ( |
| Merge packages: roles, Actions access, source label, soft delete, API | 162 | <div className="space-y-6"> |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 163 | <header className="space-y-3"> |
| 164 | <Link to={`/${pkg.workspace}/-/packages`} className="text-sm text-muted hover:text-fg"> | |
| 165 | Packages | |
| 166 | </Link> | |
| 167 | <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 start | 168 | <PackageIcon ecosystem={pkg.ecosystem} size={32} /> |
| Merge packages: roles, Actions access, source label, soft delete, API | 169 | <h1 className="min-w-0 text-2xl font-semibold tracking-tight break-all">{pkg.name}</h1> |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 170 | <Badge>{ECOSYSTEM_LABEL[pkg.ecosystem]}</Badge> |
| 171 | <Badge>{pkg.visibility === "private" ? "Private" : "Public"}</Badge> | |
| 172 | </div> | |
| 173 | <p className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted tabular-nums"> | |
| 174 | {pkg.repo && ( | |
| 175 | <Link to={`/${pkg.repo.namespace}/${pkg.repo.name}`} className="inline-flex items-center gap-1 hover:text-fg"> | |
| 176 | <Box size={13} /> | |
| 177 | {pkg.repo.namespace}/{pkg.repo.name} | |
| 178 | </Link> | |
| 179 | )} | |
| 180 | <span>{formatBytes(pkg.size)}</span> | |
| 181 | <span> | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 182 | {pkg.downloads.toLocaleString("en-US")}{" "} |
| Merge packages: roles, Actions access, source label, soft delete, API | 183 | {pkg.ecosystem !== "container" ? (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 member | 184 | </span> |
| 185 | <span> | |
| 186 | Updated <TimeAgo at={pkg.updated_at} /> | |
| 187 | </span> | |
| 188 | </p> | |
| 189 | {pkg.description && <p className="max-w-2xl text-sm text-fg-soft">{pkg.description}</p>} | |
| 190 | </header> | |
| 191 | ||
| Merge packages: roles, Actions access, source label, soft delete, API | 192 | {permissions.admin && ( |
| 193 | <TabStrip label="Package" className="gap-1 border-b border-line"> | |
| 194 | {(["overview", "settings"] as const).map((name) => ( | |
| 195 | <Link | |
| 196 | key={name} | |
| 197 | to={name === "overview" ? base : `${base}?tab=settings`} | |
| 198 | preventScrollReset | |
| 199 | aria-current={tab === name ? "page" : undefined} | |
| 200 | data-active={tab === name || undefined} | |
| 201 | className={`-mb-px border-b-2 px-3 pt-1 pb-2.5 text-sm transition-colors ${ | |
| 202 | tab === name ? "border-accent font-medium text-fg" : "border-transparent text-muted hover:text-fg" | |
| 203 | }`} | |
| 204 | > | |
| 205 | {name === "overview" ? "Overview" : "Settings"} | |
| 206 | </Link> | |
| 207 | ))} | |
| 208 | </TabStrip> | |
| 209 | )} | |
| 210 | ||
| 211 | {tab === "settings" ? ( | |
| 212 | settings && !going ? ( | |
| 213 | <PackageSettingsTab settings={settings} outcome={outcome} /> | |
| 214 | ) : ( | |
| 215 | <Loading label="Loading the package's settings…"> | |
| 216 | <PackageSettingsSkeleton /> | |
| 217 | </Loading> | |
| 218 | ) | |
| 219 | ) : ( | |
| 220 | <Overview detail={detail} username={loaderData.username} outcome={going ? undefined : outcome} /> | |
| 221 | )} | |
| 222 | </div> | |
| 223 | ); | |
| 224 | } | |
| 225 | ||
| 226 | function Overview({ | |
| 227 | detail, | |
| 228 | username, | |
| 229 | outcome, | |
| 230 | }: { | |
| 231 | detail: Route.ComponentProps["loaderData"]["detail"]; | |
| 232 | username: string; | |
| 233 | outcome: SettingsOutcome | undefined; | |
| 234 | }) { | |
| 235 | const { package: pkg, versions, tags, permissions } = detail; | |
| 236 | // Maven names a version to fetch; the others install their newest without one. | |
| 237 | const latest = tags.find((tag) => tag.tag === "latest")?.tag ?? tags[0]?.tag ?? (pkg.ecosystem === "maven" ? pkg.latest : null); | |
| 238 | const commands = installCommands(pkg, latest, username); | |
| 239 | // npm and Composer versions are numbers; images are digests and tags. | |
| 240 | const npm = pkg.ecosystem !== "container"; | |
| 241 | // Composer's versions are the repository's tags and branches: they | |
| 242 | // change in git, not here. | |
| 243 | const fromGit = pkg.ecosystem === "composer"; | |
| 244 | // Signatures and attestations hang off the images they describe. | |
| 245 | const images = versions.filter((version) => !version.subject); | |
| 246 | const attached = (digest: string) => versions.filter((version) => version.subject === digest); | |
| 247 | return ( | |
| 248 | <div className="space-y-8"> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 249 | {outcome?.error && <ErrorText>{outcome.error}</ErrorText>} |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 250 | {outcome?.message && <p className="text-sm text-success">{outcome.message}</p>} |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 251 | |
| 252 | <section className="space-y-2"> | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 253 | <h2 className="text-sm font-semibold">{npm ? "Install it" : "Pull it"}</h2> |
| 254 | {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 member | 255 | {pkg.visibility === "private" && <CopyLine prompt text={commands.login} />} |
| 256 | <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 version | 257 | {commands.registry && pkg.visibility === "private" && pkg.ecosystem === "cargo" && ( |
| 258 | <p className="text-xs text-faint"> | |
| 259 | <code className="font-mono">cargo login</code> asks for an{" "} | |
| 260 | <Link to="/settings/tokens" className="text-muted hover:text-fg"> | |
| 261 | access token | |
| 262 | </Link>{" "} | |
| 263 | with <code className="font-mono">packages:read</code>. | |
| 264 | </p> | |
| 265 | )} | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 266 | {pkg.visibility === "private" && commands.login.includes("YOUR_TOKEN") && ( |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 267 | <p className="text-xs text-faint"> |
| 268 | Put an{" "} | |
| 269 | <Link to="/settings/tokens" className="text-muted hover:text-fg"> | |
| 270 | access token | |
| 271 | </Link>{" "} | |
| 272 | with <code className="font-mono">packages:read</code> in place of YOUR_TOKEN. | |
| 273 | </p> | |
| 274 | )} | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 275 | </section> |
| 276 | ||
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 277 | {detail.readme && ( |
| 278 | <section className="overflow-hidden rounded-xl border border-line"> | |
| 279 | <h2 className="flex items-center gap-2 border-b border-line bg-surface px-4 py-2.5 text-sm font-medium"> | |
| 280 | <BookOpen size={15} className="text-faint" /> | |
| 281 | README | |
| 282 | </h2> | |
| 283 | <div className="p-6"> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 284 | <Markdown source={detail.readme} repo={pkg.repo ? { namespace: pkg.repo.namespace, name: pkg.repo.name } : undefined} /> |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 285 | </div> |
| 286 | </section> | |
| 287 | )} | |
| 288 | ||
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 289 | <section className="space-y-3"> |
| 290 | <h2 className="text-sm font-semibold"> | |
| 291 | Versions <span className="font-normal text-faint">{images.length}</span> | |
| 292 | </h2> | |
| Composer from the workspace's own repositories, and go get from g1t.sh | 293 | {fromGit && ( |
| 294 | <p className="text-xs text-faint"> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 295 | Each tag of {pkg.repo ? `${pkg.repo.namespace}/${pkg.repo.name}` : "its repository"} that reads as a version, and each branch as{" "} |
| 296 | <code className="font-mono">dev-</code>, from its composer.json. Push a tag to publish one; delete it to take it away. | |
| Composer from the workspace's own repositories, and go get from g1t.sh | 297 | </p> |
| 298 | )} | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 299 | {images.length === 0 ? ( |
| 300 | <p className="text-sm text-muted">No versions are left.</p> | |
| 301 | ) : ( | |
| 302 | <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface"> | |
| 303 | {images.map((version) => ( | |
| Composer from the workspace's own repositories, and go get from g1t.sh | 304 | <VersionRow |
| 305 | key={version.id} | |
| 306 | version={version} | |
| 307 | attached={attached(version.digest)} | |
| 308 | canDelete={permissions.delete && !fromGit} | |
| 309 | npm={npm} | |
| Merge packages: roles, Actions access, source label, soft delete, API | 310 | pulls={!npm} |
| Composer from the workspace's own repositories, and go get from g1t.sh | 311 | /> |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 312 | ))} |
| 313 | </ul> | |
| 314 | )} | |
| 315 | </section> | |
| 316 | </div> | |
| 317 | ); | |
| 318 | } | |
| 319 | ||
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 320 | function VersionRow({ |
| 321 | version, | |
| 322 | attached, | |
| 323 | canDelete, | |
| 324 | npm, | |
| Merge packages: roles, Actions access, source label, soft delete, API | 325 | pulls, |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 326 | }: { |
| 327 | version: PackageVersion; | |
| 328 | attached: PackageVersion[]; | |
| 329 | canDelete: boolean; | |
| 330 | npm: boolean; | |
| Merge packages: roles, Actions access, source label, soft delete, API | 331 | /** Whether its downloads are pulls, as an image's are. */ |
| 332 | pulls: boolean; | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 333 | }) { |
| Merge packages: roles, Actions access, source label, soft delete, API | 334 | const downloads = version.downloads ?? 0; |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 335 | return ( |
| 336 | <li className="flex flex-wrap items-start gap-x-4 gap-y-2 px-4 py-3"> | |
| 337 | <div className="min-w-0 grow space-y-1"> | |
| 338 | <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 token | 339 | {npm && <span className="font-mono text-sm font-medium">{version.version}</span>} |
| Merge packages: roles, Actions access, source label, soft delete, API | 340 | {version.tags.length > 0 |
| 341 | ? version.tags.map((tag) => ( | |
| 342 | <Badge key={tag} tone={tag === "latest" ? "accent" : "neutral"} className="font-mono"> | |
| 343 | {tag} | |
| 344 | </Badge> | |
| 345 | )) | |
| 346 | : !npm && <span className="text-xs text-faint">Untagged</span>} | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 347 | {!npm && ( |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 348 | <Hint label={version.digest}> |
| 349 | <code className="font-mono text-xs text-muted">{shortDigest(version.digest)}</code> | |
| 350 | </Hint> | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 351 | )} |
| 352 | {version.deprecated && ( | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 353 | <Hint label={version.deprecated}> |
| 354 | <Badge tone="neutral">Deprecated</Badge> | |
| 355 | </Hint> | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 356 | )} |
| Merge branch 'worktree-agent-ac1de8a731938ed81' | 357 | {version.symbols && ( |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 358 | <Hint label="A symbol package (.snupkg) was pushed: debuggers load its PDBs from the feed's symbol server."> |
| 359 | <Badge tone="neutral">Symbols</Badge> | |
| 360 | </Hint> | |
| Merge branch 'worktree-agent-ac1de8a731938ed81' | 361 | )} |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 362 | </div> |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 363 | {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 member | 364 | <p className="flex flex-wrap gap-x-3 text-xs text-faint tabular-nums"> |
| 365 | <span>{formatBytes(version.size)}</span> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 366 | <span> |
| 367 | {downloads.toLocaleString("en-US")} {pulls ? (downloads === 1 ? "pull" : "pulls") : downloads === 1 ? "download" : "downloads"} | |
| 368 | </span> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 369 | {version.platforms.length > 0 && <span>{version.platforms.join(", ")}</span>} |
| 370 | {attached.length > 0 && ( | |
| Merge branch 'worktree-agent-ae1299e92e4462012' | 371 | <Hint label={attached.map((a) => a.artifact_type ?? a.media_type ?? "artifact").join(", ")}> |
| 372 | <span> | |
| 373 | {attached.length} attached ({attached.map((a) => artifactWord(a)).join(", ")}) | |
| 374 | </span> | |
| 375 | </Hint> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 376 | )} |
| 377 | <span> | |
| 378 | {version.published_by ? `${version.published_by} · ` : ""} | |
| 379 | <TimeAgo at={version.published_at} /> | |
| 380 | </span> | |
| 381 | </p> | |
| 382 | </div> | |
| 383 | {canDelete && ( | |
| 384 | <ConfirmDialog | |
| 385 | intent="delete-version" | |
| npm on g1t.sh: publish and install @<workspace>/<name> with the npm CLI and a g1t token | 386 | fields={{ version: npm ? version.version : version.digest }} |
| 387 | title={`Delete ${npm ? version.version : (version.tags[0] ?? shortDigest(version.digest))}?`} | |
| 388 | description={ | |
| 389 | npm | |
| 390 | ? "Anyone installing this version gets an error from then on." | |
| 391 | : "Anyone pulling it by this tag or digest gets an error from then on." | |
| 392 | } | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 393 | submit="Delete version" |
| 394 | busy="Deleting…" | |
| 395 | trigger={(open) => ( | |
| Merge packages: roles, Actions access, source label, soft delete, API | 396 | <Hint label="Delete this version"> |
| 397 | <button type="button" onClick={open} aria-label="Delete version" className="rounded-md p-1.5 text-faint hover:bg-raised hover:text-danger"> | |
| 398 | <Trash2 size={14} /> | |
| 399 | </button> | |
| 400 | </Hint> | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 401 | )} |
| 402 | > | |
| 403 | <li>Its tags go with it.</li> | |
| Merge packages: roles, Actions access, source label, soft delete, API | 404 | <li>An admin can restore it from the package's Settings for {PACKAGE_RESTORE_DAYS} days; until then its {npm ? "version" : "digest"} cannot be published again.</li> |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 405 | </ConfirmDialog> |
| 406 | )} | |
| 407 | </li> | |
| 408 | ); | |
| 409 | } | |
| 410 | ||
| 411 | /** "signature", "SBOM", "attestation", or what the artifact says it is. */ | |
| 412 | function artifactWord(version: PackageVersion): string { | |
| 413 | const type = version.artifact_type ?? version.media_type ?? ""; | |
| 414 | if (/signature|cosign|notary/i.test(type)) return "signature"; | |
| 415 | if (/spdx|cyclonedx|sbom/i.test(type)) return "SBOM"; | |
| 416 | if (/in-toto|attestation|provenance/i.test(type)) return "attestation"; | |
| 417 | return "artifact"; | |
| 418 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.