Files
thatguygriffandClaude Opus 5 0506f44db3 Build the Apple Container extension
Turns the empty scaffold into a working Nova extension for Apple Container
and container-compose, in the shape of the Docker Suite extension.

Sidebar (Scripts/Sidebar):
- System, Containers, Images, Volumes and Networks sections
- Containers grouped into compose projects via the com.docker.compose.project
  label container-compose stamps; project rows drive compose up/down/build
  against the matching workspace file
- Lifecycle, shell, logs, inspect, browse and copy commands per resource
- Apple Container publishes no event stream, so the sidebar polls while
  visible and only reloads a section when a fingerprint of its data changes,
  which keeps selection and expansion intact

Hostnames (Scripts/Hostnames.js):
- A container's hostname is <name>.<domain> and the domain lives in Apple
  Container's config.toml, which has no CLI setter, so the Hostname Domain
  preference reads and writes that file directly
- Changing it offers to register the domain with macOS and restart the
  services; the write preserves the file's other tables and its mode

Language support:
- Dockerfile and Compose syntaxes backed by tree-sitter grammars built by
  Tools/build-syntaxes.sh, pinned to revisions that generate ABI 14
- The Compose syntax is named dockercompose because that is the languageId
  docker-language-server recognises compose files by
- docker-language-server is downloaded on first use rather than bundled, at
  roughly 40 MB

Icons are generated by Tools/make-icons.py so they can be regenerated rather
than hand-maintained.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UnSBcR5Lywz5Fbj5FmVbfc
2026-08-18 22:45:14 -03:00

120 lines
3.1 KiB
JavaScript

//
// Images, optionally grouped by repository.
//
const { Element } = require("./Element");
const format = require("../../Utilities/Format");
const INFRA_PREFIXES = [
"ghcr.io/apple/container-builder-shim/",
"ghcr.io/apple/containerization/vminit"
];
class ImageModel extends Element {
constructor(json) {
let configuration = (json && json.configuration) || {};
super(`image:${configuration.name}`);
this.reference = configuration.name || "";
this.digest = (configuration.descriptor && configuration.descriptor.digest) || "";
// The CLI identifies images by the first twelve hex digits of the digest.
this.shortDigest = this.digest.replace(/^[a-z0-9]+:/, "").slice(0, 12);
this.id = json.id || "";
this.created = configuration.creationDate || null;
let parts = format.reference(this.reference);
this.registry = parts.registry;
this.repository = parts.repository;
this.tag = parts.tag;
this.short = parts.short;
this.variants = (json.variants || []).map((variant) => ({
architecture: (variant.platform && variant.platform.architecture) || "",
os: (variant.platform && variant.platform.os) || "",
size: variant.size || 0
}));
this.size = this.variants.reduce((total, variant) => total + variant.size, 0);
}
get revision() {
return `${this.digest}:${this.size}`;
}
get infrastructure() {
return INFRA_PREFIXES.some((prefix) => this.reference.startsWith(prefix));
}
get platforms() {
return this.variants
.map((variant) => (variant.os ? `${variant.os}/${variant.architecture}` : variant.architecture))
.filter((platform) => platform.length > 0)
.join(" ");
}
get tooltip() {
let lines = [`Image:\t${this.reference}`, `Digest:\t${this.shortDigest}`];
if (this.platforms) {
lines.push(`Platform:\t${this.platforms}`);
}
if (this.size) {
lines.push(`Size:\t${format.bytes(this.size)}`);
}
if (this.created) {
lines.push(`Created:\t${format.age(this.created)}`);
}
return lines.join("\n");
}
}
class RepositoryModel extends Element {
constructor(name) {
super(`repository:${name}`);
this.name = name;
this.expanded = true;
}
get size() {
return this.children.reduce((total, image) => total + image.size, 0);
}
get revision() {
return String(this.size);
}
}
function build(list, options = {}) {
let images = (list || []).map((entry) => new ImageModel(entry));
if (!options.infrastructure) {
images = images.filter((image) => !image.infrastructure);
}
let label = (image) => (options.registry ? image.reference : `${image.short}:${image.tag}`);
images.sort((left, right) => label(left).localeCompare(label(right)));
if (!options.group) {
return images;
}
let repositories = new Map();
for (let image of images) {
let name = options.registry && image.registry ? `${image.registry}/${image.repository}` : image.short;
if (!repositories.has(name)) {
repositories.set(name, new RepositoryModel(name));
}
repositories.get(name).addChild(image);
}
return [...repositories.values()].sort((left, right) => left.name.localeCompare(right.name));
}
module.exports = { ImageModel, RepositoryModel, build };