| 470 | |
| 471 | /** Parse a compose `image:` value, with the `${VAR:-default}` pin resolved. */ |
| 472 | export function parseImage(image: string): ImageRef | null { |
| 473 | const raw = image.trim(); |
| 474 | if (!raw) return null; |
| 475 | const at = raw.lastIndexOf("@"); |
| 476 | const withoutDigest = at > 0 ? raw.slice(0, at) : raw; |
| 477 | const lastColon = withoutDigest.lastIndexOf(":"); |
| 478 | const lastSlash = withoutDigest.lastIndexOf("/"); |
| 479 | const tag = lastColon > lastSlash ? withoutDigest.slice(lastColon + 1) : "latest"; |
| 480 | const path = lastColon > lastSlash ? withoutDigest.slice(0, lastColon) : withoutDigest; |
| 481 | |
| 482 | const [maybeHost, ...rest] = path.split("/"); |
| 483 | if (rest.length && (maybeHost.includes(".") || maybeHost.includes(":"))) { |
| 484 | const name = rest.join("/"); |
| 485 | if (maybeHost === "ghcr.io") return { registry: "ghcr", name, tag, raw }; |
| 486 | if (maybeHost === "quay.io") return { registry: "quay", name, tag, raw }; |
| 487 | if (maybeHost.endsWith("docker.io")) |
| 488 | return { registry: "dockerhub", name: name.includes("/") ? name : `library/${name}`, tag, raw }; |
| 489 | return { registry: "other", name, tag, raw }; |
| 490 | } |
| 491 | return { |
| 492 | registry: "dockerhub", |
| 493 | name: path.includes("/") ? path : `library/${path}`, |
| 494 | tag, |
| 495 | raw, |
| 496 | }; |
| 497 | } |
| 498 | |
| 499 | /** Tags available for an image, newest-first where the registry allows it. */ |
| 500 | export async function fetchImageTags( |