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

108 lines
2.2 KiB
JavaScript

//
// Formatting helpers shared by the tree items.
//
const UNITS = ["B", "KB", "MB", "GB", "TB", "PB"];
function bytes(value) {
if (typeof value !== "number" || !isFinite(value) || value < 0) {
return "";
}
let index = 0;
let size = value;
while (size >= 1000 && index < UNITS.length - 1) {
size = size / 1000;
index += 1;
}
let precision = size >= 100 || index === 0 ? 0 : 1;
return `${size.toFixed(precision)} ${UNITS[index]}`;
}
// Relative age, in the same spirit as Docker's "2 hours ago".
function age(value) {
if (!value) {
return "";
}
let date = value instanceof Date ? value : new Date(value);
if (isNaN(date.getTime()) || date.getTime() === 0) {
return "";
}
let seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
if (seconds < 60) {
return "just now";
}
let steps = [
["minute", 60],
["hour", 60],
["day", 24],
["week", 7],
["month", 4.35],
["year", 12]
];
let amount = seconds;
let label = "second";
for (let [name, divisor] of steps) {
if (amount < divisor) {
break;
}
amount = amount / divisor;
label = name;
}
let rounded = Math.floor(amount);
return `${rounded} ${label}${rounded === 1 ? "" : "s"} ago`;
}
// Splits an image reference into its registry, repository and tag.
function reference(value) {
let text = String(value || "");
let digest = null;
let tag = null;
let atIndex = text.lastIndexOf("@");
if (atIndex > 0) {
digest = text.slice(atIndex + 1);
text = text.slice(0, atIndex);
}
let colonIndex = text.lastIndexOf(":");
let slashIndex = text.lastIndexOf("/");
if (colonIndex > slashIndex) {
tag = text.slice(colonIndex + 1);
text = text.slice(0, colonIndex);
}
let registry = null;
let repository = text;
let firstSlash = text.indexOf("/");
if (firstSlash > 0) {
let candidate = text.slice(0, firstSlash);
if (candidate.includes(".") || candidate.includes(":") || candidate === "localhost") {
registry = candidate;
repository = text.slice(firstSlash + 1);
}
}
return {
registry: registry,
repository: repository,
tag: tag || "latest",
digest: digest,
// docker.io/library/nginx reads better as nginx.
short: repository.replace(/^library\//, "")
};
}
module.exports = { bytes, age, reference };