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
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user