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
184 lines
4.4 KiB
JavaScript
184 lines
4.4 KiB
JavaScript
//
|
|
// Language support for Dockerfiles and Compose files, backed by
|
|
// docker-language-server (github.com/docker/docker-language-server).
|
|
//
|
|
// The binary is a single ~40 MB executable, which is too large to ship inside
|
|
// the extension, so the managed mode downloads the pinned release into the
|
|
// extension's global storage the first time it is needed.
|
|
//
|
|
|
|
const { exec } = require("../Utilities/Process");
|
|
const notify = require("../Utilities/Notify");
|
|
|
|
const RELEASE_URL = "https://github.com/docker/docker-language-server/releases/download";
|
|
// docker-language-server decides what a document is from the languageId the
|
|
// editor sends, and Nova sends the syntax name. Compose support only switches
|
|
// on for "dockercompose", which is why the Compose syntax carries that name.
|
|
const SYNTAXES = ["dockerfile", "dockercompose"];
|
|
|
|
class DockerLanguageClient {
|
|
constructor() {
|
|
this.client = null;
|
|
this.disposables = new CompositeDisposable();
|
|
|
|
for (let key of ["apple_container.lsp.mode", "apple_container.lsp.path", "apple_container.lsp.version"]) {
|
|
this.disposables.add(nova.config.onDidChange(key, () => this.restart()));
|
|
}
|
|
}
|
|
|
|
get mode() {
|
|
return nova.config.get("apple_container.lsp.mode", "string") || "Managed Binary";
|
|
}
|
|
|
|
get version() {
|
|
return (nova.config.get("apple_container.lsp.version", "string") || "").trim() || "v0.20.1";
|
|
}
|
|
|
|
async start() {
|
|
if (this.mode === "Disabled") {
|
|
return;
|
|
}
|
|
|
|
let path = null;
|
|
|
|
try {
|
|
path = this.mode === "Custom Path" ? this.customPath() : await this.managedPath();
|
|
} catch (error) {
|
|
notify.configFailure("Docker Language Server unavailable", error, "apple-container-lsp");
|
|
return;
|
|
}
|
|
|
|
if (!path) {
|
|
return;
|
|
}
|
|
|
|
this.stop();
|
|
|
|
try {
|
|
this.client = new LanguageClient(
|
|
"unsupervised.AppleContainer.dls",
|
|
"Docker Language Server",
|
|
{
|
|
path: path,
|
|
args: ["start", "--stdio"],
|
|
type: "stdio"
|
|
},
|
|
{
|
|
syntaxes: SYNTAXES
|
|
}
|
|
);
|
|
|
|
this.client.onDidStop((error) => {
|
|
if (error) {
|
|
notify.failure(
|
|
"Docker Language Server stopped",
|
|
error,
|
|
"apple-container-lsp-stopped"
|
|
);
|
|
}
|
|
});
|
|
|
|
this.client.start();
|
|
nova.subscriptions.add(this.client);
|
|
} catch (error) {
|
|
notify.failure("Docker Language Server failed to launch", error, "apple-container-lsp");
|
|
this.client = null;
|
|
}
|
|
}
|
|
|
|
stop() {
|
|
if (!this.client) {
|
|
return;
|
|
}
|
|
|
|
this.client.stop();
|
|
nova.subscriptions.remove(this.client);
|
|
this.client = null;
|
|
}
|
|
|
|
async restart() {
|
|
this.stop();
|
|
await this.start();
|
|
}
|
|
|
|
dispose() {
|
|
this.stop();
|
|
this.disposables.dispose();
|
|
}
|
|
|
|
customPath() {
|
|
let path = (nova.config.get("apple_container.lsp.path", "string") || "").trim();
|
|
|
|
if (!path) {
|
|
throw new Error("Set the path to docker-language-server in the extension's preferences.");
|
|
}
|
|
|
|
if (!nova.fs.access(path, nova.fs.X_OK)) {
|
|
throw new Error(`${path} is not an executable file.`);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
// Returns the managed binary, downloading it when it is missing.
|
|
async managedPath() {
|
|
let architecture = await this.architecture();
|
|
let name = `docker-language-server-darwin-${architecture}-${this.version}`;
|
|
let directory = nova.extension.globalStoragePath;
|
|
let path = nova.path.join(directory, name);
|
|
|
|
if (nova.fs.access(path, nova.fs.X_OK)) {
|
|
return path;
|
|
}
|
|
|
|
if (!nova.fs.access(directory, nova.fs.F_OK)) {
|
|
nova.fs.mkdir(directory);
|
|
}
|
|
|
|
await this.download(`${RELEASE_URL}/${this.version}/${name}`, path);
|
|
|
|
return path;
|
|
}
|
|
|
|
async architecture() {
|
|
let result = await exec("/usr/bin/uname", ["-m"]);
|
|
return result.stdout.trim() === "x86_64" ? "amd64" : "arm64";
|
|
}
|
|
|
|
async download(url, destination) {
|
|
notify.info(
|
|
"Downloading Docker Language Server",
|
|
`${this.version} for Dockerfile and Compose support. This happens once.`,
|
|
"apple-container-lsp-download"
|
|
);
|
|
|
|
// A partial file would look like a working binary on the next launch,
|
|
// so download beside the target and move it into place at the end.
|
|
let temporary = `${destination}.download`;
|
|
|
|
try {
|
|
await exec("/usr/bin/curl", [
|
|
"--fail",
|
|
"--location",
|
|
"--silent",
|
|
"--show-error",
|
|
"--output",
|
|
temporary,
|
|
url
|
|
]);
|
|
await exec("/bin/chmod", ["+x", temporary]);
|
|
nova.fs.move(temporary, destination);
|
|
} catch (error) {
|
|
try {
|
|
nova.fs.remove(temporary);
|
|
} catch (cleanupError) {
|
|
// Nothing to clean up.
|
|
}
|
|
|
|
throw new Error(`Could not download ${url}: ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { DockerLanguageClient };
|