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:
2026-08-18 22:45:14 -03:00
co-authored by Claude Opus 5
parent 0879e5a163
commit 0506f44db3
80 changed files with 5201 additions and 3 deletions
+89
View File
@@ -0,0 +1,89 @@
//
// Thin promise wrappers around Nova's Process API.
//
// Runs a command to completion and resolves with its captured output.
// Rejects with an Error carrying the exit status and stderr when the
// command fails, so callers can surface a useful message.
function exec(path, args, options = {}) {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
let process;
try {
process = new Process(path, {
args: args,
cwd: options.cwd,
env: options.env,
stdio: ["ignore", "pipe", "pipe"]
});
} catch (error) {
reject(error);
return;
}
process.onStdout((line) => {
stdout += line;
});
process.onStderr((line) => {
stderr += line;
});
process.onDidExit((status) => {
if (status === 0 || options.ignoreStatus) {
resolve({ status: status, stdout: stdout, stderr: stderr });
return;
}
let error = new Error(cleanMessage(stderr) || `${path} exited with status ${status}.`);
error.status = status;
error.stdout = stdout;
error.stderr = stderr;
reject(error);
});
try {
process.start();
} catch (error) {
reject(error);
}
});
}
// Runs a command and parses its stdout as JSON.
async function execJSON(path, args, options = {}) {
let result = await exec(path, args, options);
if (!result.stdout.trim()) {
return [];
}
try {
return JSON.parse(result.stdout);
} catch (error) {
throw new Error(`Unable to parse output of ${path}: ${error.message}`);
}
}
// Apple Container prefixes failures with "Error: " and appends a hint about
// starting the system services. Keep the first meaningful line only.
function cleanMessage(stderr) {
if (!stderr) {
return "";
}
let line = stderr
.split("\n")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0)[0];
if (!line) {
return "";
}
return line.replace(/^Error:\s*/, "");
}
module.exports = { exec, execJSON, cleanMessage };