Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/guides/vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ Dev Container extension hosts. Browser-only `vscode.dev` is not supported.
1. Install Compass and confirm `compass --version` works on the workspace host.
2. Install the Compass VSIX.
3. Open a trusted repository.
4. If the extension cannot find Compass on `PATH`, choose **Select Compass
Binary** or set `compass.cliPath`.
4. The extension detects Compass on `PATH` and in common install locations.
Choose **Select Compass CLI** to compare detected paths and versions, browse
for another executable, enter a path manually, or set `compass.cliPath`
directly.
5. Open the Compass activity bar and run **Initialize Repository**.

Initialization previews include and exclude globs before it writes
`.compass/config.toml`. The extension never installs a CLI automatically.
The CLI must support `compass capabilities --format json`. If capability
negotiation fails or a required versioned contract is missing, the extension
does not run the incompatible command; upgrade Compass or use **Compass: Select
CLI Binary**, then reload VS Code.
CLI Version** to choose another detected version, then reload VS Code.

## Current graph

Expand Down
6 changes: 3 additions & 3 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
},
{
"command": "compass.selectCli",
"title": "Compass: Select CLI Binary",
"title": "Compass: Select CLI Version",
"icon": "$(terminal)"
}
],
Expand All @@ -143,7 +143,7 @@
"type": "string",
"default": "",
"scope": "machine",
"description": "Absolute path to an installed Compass CLI. Leave empty to discover compass on PATH."
"description": "Absolute path to the selected Compass CLI. Leave empty to auto-detect Compass on PATH and in common install locations."
},
"compass.graphNodeLimit": {
"type": "number",
Expand Down Expand Up @@ -255,7 +255,7 @@
{
"id": "compass.cli",
"title": "Connect the Compass CLI",
"description": "Install Compass separately and configure `compass.cliPath` if it is not on PATH."
"description": "Compass auto-detects installed CLI versions. Use **Select Compass CLI** to compare paths or choose a different version."
},
{
"id": "compass.init",
Expand Down
164 changes: 127 additions & 37 deletions editors/vscode/src/cli/discovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import path from "node:path";
import { mkdir, writeFile } from "node:fs/promises";
import { chmodSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";

import { discoverCompass } from "./discovery";
import {
discoverCompass,
inspectCompassInstallation,
resolveCompassPath
} from "./discovery";

const created: string[] = [];
afterEach(async () => {
Expand All @@ -15,54 +19,140 @@ afterEach(async () => {
});

describe("discoverCompass", () => {
it("prefers the configured executable", async () => {
const directory = path.join(process.cwd(), `.tmp-discovery-${Date.now()}`);
created.push(directory);
await mkdir(directory);
const executable = path.join(directory, "compass");
await writeFile(executable, "#!/bin/sh\n");
chmodSync(executable, 0o755);
const result = await discoverCompass({ get: () => executable });
expect(result).toEqual({ kind: "found", executable });
it("finds every installed version and keeps the configured executable active", async () => {
const directory = await temporaryDirectory("versions");
const pathDirectory = path.join(directory, "path");
await mkdir(pathDirectory);
const configured = path.join(directory, "configured-compass");
const fromPath = path.join(pathDirectory, "compass");
await Promise.all([
writeCompass(configured, "0.1.4"),
writeCompass(fromPath, "0.2.0")
]);

const result = await discoverCompass(
{ get: () => configured },
{ PATH: pathDirectory },
"darwin",
{ commonDirectories: [] }
);

expect(result).toEqual({
kind: "found",
executable: configured,
version: "0.1.4",
installations: [
{
executable: configured,
version: "0.1.4",
source: "configured"
},
{
executable: fromPath,
version: "0.2.0",
source: "path"
}
],
searched: [configured, fromPath]
});
});

it("falls back to PATH when the configured executable is unavailable", async () => {
const directory = path.join(process.cwd(), `.tmp-discovery-fallback-${Date.now()}`);
created.push(directory);
await mkdir(directory);
const directory = await temporaryDirectory("fallback");
const executable = path.join(directory, "compass");
await writeFile(executable, "#!/bin/sh\n");
chmodSync(executable, 0o755);
await writeCompass(executable, "0.1.5");
const missing = path.join(directory, "missing-compass");

const result = await discoverCompass(
{ get: () => path.join(directory, "missing-compass") },
{ get: () => missing },
{ PATH: directory },
"darwin"
"darwin",
{ commonDirectories: [] }
);

expect(result).toEqual({ kind: "found", executable });
expect(result).toMatchObject({
kind: "found",
executable,
version: "0.1.5"
});
expect(result.installations).toEqual([{
executable,
version: "0.1.5",
source: "path"
}]);
});

it("keeps a working configured executable ahead of PATH", async () => {
const directory = path.join(process.cwd(), `.tmp-discovery-priority-${Date.now()}`);
const pathDirectory = path.join(directory, "path");
created.push(directory);
await mkdir(pathDirectory, { recursive: true });
const configured = path.join(directory, "configured-compass");
const fromPath = path.join(pathDirectory, "compass");
await Promise.all([
writeFile(configured, "#!/bin/sh\n"),
writeFile(fromPath, "#!/bin/sh\n")
]);
chmodSync(configured, 0o755);
chmodSync(fromPath, 0o755);
it("detects the default install directory even when it is not on PATH", async () => {
const directory = await temporaryDirectory("common");
const executable = path.join(directory, "compass");
await writeCompass(executable, "0.3.1");

const result = await discoverCompass(
{ get: () => configured },
{ PATH: pathDirectory },
"darwin"
{ get: () => "" },
{ PATH: "" },
"linux",
{ commonDirectories: [directory] }
);

expect(result).toEqual({ kind: "found", executable: configured });
expect(result).toMatchObject({
kind: "found",
executable,
version: "0.3.1",
installations: [{
executable,
version: "0.3.1",
source: "common"
}]
});
});

it("deduplicates a configured executable that is also on PATH", async () => {
const directory = await temporaryDirectory("deduplicate");
const executable = path.join(directory, "compass");
await writeCompass(executable, "0.1.5");

const result = await discoverCompass(
{ get: () => executable },
{ PATH: directory },
"darwin",
{ commonDirectories: [directory] }
);

expect(result.installations).toHaveLength(1);
expect(result.installations[0]?.source).toBe("configured");
});

it("keeps an executable discoverable when its version cannot be read", async () => {
const directory = await temporaryDirectory("unknown-version");
const executable = path.join(directory, "compass");
await writeFile(executable, "#!/bin/sh\nexit 1\n");
chmodSync(executable, 0o755);

await expect(inspectCompassInstallation(executable)).resolves.toEqual({
executable,
source: "configured"
});
});

it("expands a home-relative path entered by the user", () => {
expect(resolveCompassPath(
"~/.local/bin/compass",
{ HOME: "/home/dev" }
)).toBe(path.resolve("/home/dev/.local/bin/compass"));
});
});

async function temporaryDirectory(label: string): Promise<string> {
const directory = path.join(
process.cwd(),
`.tmp-discovery-${label}-${Date.now()}-${created.length}`
);
created.push(directory);
await mkdir(directory);
return directory;
}

async function writeCompass(executable: string, version: string): Promise<void> {
await writeFile(executable, `#!/bin/sh\necho 'compass ${version}'\n`);
chmodSync(executable, 0o755);
}
Loading
Loading