Skip to content

Commit a3513f3

Browse files
committed
feat: managed install path fix, competitive gap improvements
Fix managed install path mismatch by removing version directory layer. Binary now installs to <root>/managed-bin/patchloom instead of <root>/<version>/managed-bin/patchloom, preventing discovery from looking at the wrong version directory. Add 10 improvements identified from competitive analysis of vscode-go, rust-analyzer, vscode-terraform, ruff, biome, deno, and cody: - More Marketplace categories (Linters, Machine Learning) - Content-based activation (workspaceContains) instead of onStartupFinished - Getting Started walkthrough with 3 steps and completion events - CLI version shown in status bar text (e.g. Patchloom v0.1.5) - Open Documentation command - New settings: enable, trace.server, env, managedInstall.autoUpdate - Context keys and when clauses to hide irrelevant commands - Auto-update check on activation for managed installs - Untrusted workspace policy: restricts to managed binary only - Native MCP server registration via mcpServerDefinitionProviders (1.100+) Closes #103 Closes #104 Closes #105 Closes #106 Closes #107 Closes #108 Closes #109 Closes #110 Closes #111 Closes #112 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent eda9e6b commit a3513f3

16 files changed

Lines changed: 431 additions & 102 deletions

package.json

Lines changed: 109 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
},
2525
"categories": [
2626
"Formatters",
27+
"Linters",
28+
"Machine Learning",
2729
"Other"
2830
],
2931
"keywords": [
@@ -39,20 +41,8 @@
3941
],
4042
"main": "./out/extension.js",
4143
"activationEvents": [
42-
"onStartupFinished",
43-
"onCommand:patchloom.initializeProject",
44-
"onCommand:patchloom.setupWorkspace",
45-
"onCommand:patchloom.configureMcp",
46-
"onCommand:patchloom.quickAction",
47-
"onCommand:patchloom.openPatchloomSettings",
48-
"onCommand:patchloom.openPatchloomReleases",
49-
"onCommand:patchloom.showStatus",
50-
"onCommand:patchloom.batchApply",
51-
"onCommand:patchloom.showOutput",
52-
"onCommand:patchloom.installBinary",
53-
"onCommand:patchloom.updateBinary",
54-
"onCommand:patchloom.reinstallBinary",
55-
"onCommand:patchloom.verifyMcp"
44+
"workspaceContains:**/AGENTS.md",
45+
"workspaceContains:**/.patchloom.toml"
5646
],
5747
"contributes": {
5848
"commands": [
@@ -120,6 +110,90 @@
120110
"command": "patchloom.verifyMcp",
121111
"title": "Verify MCP Server",
122112
"category": "Patchloom"
113+
},
114+
{
115+
"command": "patchloom.openDocumentation",
116+
"title": "Open Documentation",
117+
"category": "Patchloom"
118+
}
119+
],
120+
"menus": {
121+
"commandPalette": [
122+
{
123+
"command": "patchloom.installBinary",
124+
"when": "!patchloom.cliAvailable"
125+
},
126+
{
127+
"command": "patchloom.updateBinary",
128+
"when": "patchloom.managedInstallExists"
129+
},
130+
{
131+
"command": "patchloom.reinstallBinary",
132+
"when": "patchloom.managedInstallExists"
133+
},
134+
{
135+
"command": "patchloom.quickAction",
136+
"when": "patchloom.cliAvailable"
137+
},
138+
{
139+
"command": "patchloom.batchApply",
140+
"when": "patchloom.cliAvailable"
141+
},
142+
{
143+
"command": "patchloom.initializeProject",
144+
"when": "patchloom.cliAvailable"
145+
},
146+
{
147+
"command": "patchloom.configureMcp",
148+
"when": "patchloom.cliAvailable"
149+
},
150+
{
151+
"command": "patchloom.verifyMcp",
152+
"when": "patchloom.cliAvailable"
153+
}
154+
]
155+
},
156+
"walkthroughs": [
157+
{
158+
"id": "patchloom.gettingStarted",
159+
"title": "Get Started with Patchloom",
160+
"description": "Set up Patchloom in your workspace for AI agent workflows.",
161+
"steps": [
162+
{
163+
"id": "installCli",
164+
"title": "Install the Patchloom CLI",
165+
"description": "Install the Patchloom CLI via the managed installer, Homebrew, or cargo.\n\n[Install Patchloom](command:patchloom.installBinary)",
166+
"media": {
167+
"markdown": "walkthrough/install.md"
168+
},
169+
"completionEvents": [
170+
"onCommand:patchloom.installBinary",
171+
"onContext:patchloom.cliAvailable"
172+
]
173+
},
174+
{
175+
"id": "initializeProject",
176+
"title": "Initialize Your Project",
177+
"description": "Generate an AGENTS.md file with agent rules for your workspace.\n\n[Initialize Project](command:patchloom.initializeProject)",
178+
"media": {
179+
"markdown": "walkthrough/initialize.md"
180+
},
181+
"completionEvents": [
182+
"onCommand:patchloom.initializeProject"
183+
]
184+
},
185+
{
186+
"id": "configureMcp",
187+
"title": "Configure MCP Server",
188+
"description": "Set up the Patchloom MCP server so AI agents can use it.\n\n[Configure MCP](command:patchloom.configureMcp)",
189+
"media": {
190+
"markdown": "walkthrough/configure-mcp.md"
191+
},
192+
"completionEvents": [
193+
"onCommand:patchloom.configureMcp"
194+
]
195+
}
196+
]
123197
}
124198
],
125199
"configuration": {
@@ -134,6 +208,27 @@
134208
"type": "boolean",
135209
"default": true,
136210
"description": "Show a status bar item reporting whether Patchloom is available for the current workspace."
211+
},
212+
"patchloom.enable": {
213+
"type": "boolean",
214+
"default": true,
215+
"markdownDescription": "Enable the Patchloom extension. When disabled, the status bar is hidden and background checks are skipped. Commands remain available for manual invocation."
216+
},
217+
"patchloom.trace.server": {
218+
"type": "string",
219+
"default": "off",
220+
"enum": ["off", "messages", "verbose"],
221+
"markdownDescription": "Trace level for Patchloom CLI output. `messages` logs command invocations and results. `verbose` includes full stdout/stderr."
222+
},
223+
"patchloom.env": {
224+
"type": "object",
225+
"default": {},
226+
"markdownDescription": "Additional environment variables passed to the Patchloom CLI. For example, `{\"PATCHLOOM_LOG\": \"debug\"}` enables debug logging in the CLI."
227+
},
228+
"patchloom.managedInstall.autoUpdate": {
229+
"type": "boolean",
230+
"default": true,
231+
"markdownDescription": "Automatically check for Patchloom CLI updates when the extension activates. Shows a notification when a newer version is available."
137232
}
138233
}
139234
}

src/binary/patchloom.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const execFileAsync = promisify(execFile);
1515

1616
export const MINIMUM_SUPPORTED_PATCHLOOM_VERSION = "0.1.0";
1717
export const PATCHLOOM_RELEASES_URL = "https://github.com/patchloom/patchloom/releases";
18+
export const PATCHLOOM_DOCS_URL = "https://github.com/patchloom/patchloom#readme";
1819

1920
export type PatchloomSource = "setting" | "path" | "managed" | "missing";
2021
export type PatchloomCompatibility = "supported" | "unsupported" | "unknown";
@@ -48,8 +49,8 @@ export interface PatchloomStatusInputs {
4849
readonly canExecute?: (binaryPath: string) => Promise<boolean>;
4950
readonly getVersion?: (binaryPath: string) => Promise<string | undefined>;
5051
readonly managedInstallRoot?: string;
51-
readonly managedInstallVersion?: string;
5252
readonly managedFileExists?: (filePath: string) => Promise<boolean>;
53+
readonly isTrusted?: boolean;
5354
}
5455

5556
export async function resolvePatchloomStatus(): Promise<PatchloomStatus> {
@@ -61,18 +62,18 @@ export async function resolvePatchloomStatus(): Promise<PatchloomStatus> {
6162
platform: process.platform,
6263
arch: process.arch,
6364
managedInstallRoot,
64-
managedInstallVersion: MINIMUM_SUPPORTED_PATCHLOOM_VERSION
65+
isTrusted: vscode.workspace.isTrusted
6566
});
6667
}
6768

6869
export async function resolvePatchloomStatusWithInputs(inputs: PatchloomStatusInputs): Promise<PatchloomStatus> {
69-
const configuredPath = configuredBinaryPathFromSetting(inputs.configuredPath);
70+
const isTrusted = inputs.isTrusted ?? true;
71+
const configuredPath = isTrusted ? configuredBinaryPathFromSetting(inputs.configuredPath) : undefined;
7072
const canExecute = inputs.canExecute ?? isExecutable;
7173
const getVersion = inputs.getVersion ?? readVersion;
7274
const managedInstall = inputs.managedInstallRoot
7375
? await inspectManagedInstallStatus({
7476
installRoot: inputs.managedInstallRoot,
75-
version: inputs.managedInstallVersion,
7677
target: detectManagedInstallTarget(inputs.platform, inputs.arch),
7778
fileExists: inputs.managedFileExists,
7879
failurePersistence: {
@@ -87,23 +88,29 @@ export async function resolvePatchloomStatusWithInputs(inputs: PatchloomStatusIn
8788
return withManagedInstallContext(status, managedInstall, diagnostics);
8889
}
8990

90-
const discoveredPath = await findOnPath(inputs.pathValue, inputs.platform, canExecute);
91-
if (discoveredPath) {
92-
const status = await inspectCandidate(discoveredPath, "path", canExecute, getVersion);
93-
return withManagedInstallContext(status, managedInstall, diagnostics);
91+
if (isTrusted) {
92+
const discoveredPath = await findOnPath(inputs.pathValue, inputs.platform, canExecute);
93+
if (discoveredPath) {
94+
const status = await inspectCandidate(discoveredPath, "path", canExecute, getVersion);
95+
return withManagedInstallContext(status, managedInstall, diagnostics);
96+
}
9497
}
9598

9699
if (managedInstall?.exists) {
97100
const status = await inspectCandidate(managedInstall.binaryPath, "managed", canExecute, getVersion);
98101
return withManagedInstallContext(status, managedInstall, diagnostics);
99102
}
100103

104+
const notFoundMessage = !isTrusted
105+
? "Patchloom is restricted to the managed install in untrusted workspaces. Install via the managed installer or grant workspace trust."
106+
: managedInstall
107+
? "Patchloom binary not found. Set patchloom.path, install patchloom on PATH, or install a managed Patchloom release."
108+
: "Patchloom binary not found. Set patchloom.path or install patchloom on PATH.";
109+
101110
return {
102111
ready: false,
103112
source: "missing",
104-
message: managedInstall
105-
? `Patchloom binary not found. Set patchloom.path, install patchloom on PATH, or install a managed Patchloom release.`
106-
: "Patchloom binary not found. Set patchloom.path or install patchloom on PATH.",
113+
message: notFoundMessage,
107114
compatibility: "unknown",
108115
minimumSupportedVersion: MINIMUM_SUPPORTED_PATCHLOOM_VERSION,
109116
managedInstall,

src/commands/autoUpdate.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as vscode from "vscode";
2+
import { comparePatchloomVersions, PATCHLOOM_RELEASES_URL, resolvePatchloomStatus } from "../binary/patchloom.js";
3+
import { fetchLatestReleaseVersion, getManagedInstallRoot } from "../install/managed.js";
4+
import { getPatchloomLog } from "../logging/outputChannel.js";
5+
6+
export async function checkForUpdates(): Promise<void> {
7+
const config = vscode.workspace.getConfiguration("patchloom");
8+
if (!config.get<boolean>("enable", true)) {
9+
return;
10+
}
11+
if (!config.get<boolean>("managedInstall.autoUpdate", true)) {
12+
return;
13+
}
14+
15+
const installRoot = getManagedInstallRoot();
16+
if (!installRoot) {
17+
return;
18+
}
19+
20+
const status = await resolvePatchloomStatus();
21+
if (!status.ready || !status.detectedVersion || status.source !== "managed") {
22+
return;
23+
}
24+
25+
const log = getPatchloomLog();
26+
try {
27+
const latestVersion = await fetchLatestReleaseVersion();
28+
if (comparePatchloomVersions(latestVersion, status.detectedVersion) <= 0) {
29+
return;
30+
}
31+
32+
log?.log(`Update available: ${status.detectedVersion} -> ${latestVersion}`);
33+
const choice = await vscode.window.showInformationMessage(
34+
`Patchloom v${latestVersion} is available (current: v${status.detectedVersion}).`,
35+
"Update Now",
36+
"View Release"
37+
);
38+
39+
if (choice === "Update Now") {
40+
await vscode.commands.executeCommand("patchloom.updateBinary");
41+
} else if (choice === "View Release") {
42+
await vscode.env.openExternal(
43+
vscode.Uri.parse(`${PATCHLOOM_RELEASES_URL}/tag/patchloom-v${latestVersion}`)
44+
);
45+
}
46+
} catch (error) {
47+
log?.log(`Auto-update check failed: ${error instanceof Error ? error.message : String(error)}`);
48+
}
49+
}

src/commands/setupWorkspace.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from "vscode";
2-
import { PATCHLOOM_RELEASES_URL, patchloomNeedsUpgrade, resolvePatchloomStatus } from "../binary/patchloom.js";
2+
import { PATCHLOOM_DOCS_URL, PATCHLOOM_RELEASES_URL, patchloomNeedsUpgrade, resolvePatchloomStatus } from "../binary/patchloom.js";
33
import { describeWorkspaceEnvironment, inspectWorkspaceReadiness } from "../workspace/readiness.js";
44

55
export async function setupWorkspace(): Promise<void> {
@@ -74,3 +74,7 @@ export async function openPatchloomSettings(): Promise<void> {
7474
export async function openPatchloomReleases(): Promise<void> {
7575
await vscode.env.openExternal(vscode.Uri.parse(PATCHLOOM_RELEASES_URL));
7676
}
77+
78+
export async function openDocumentation(): Promise<void> {
79+
await vscode.env.openExternal(vscode.Uri.parse(PATCHLOOM_DOCS_URL));
80+
}

src/extension.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { configureMcp } from "./commands/configureMcp.js";
44
import { initializeProject } from "./commands/initializeProject.js";
55
import { installPatchloom, updatePatchloom, reinstallPatchloom } from "./commands/managedInstall.js";
66
import { runQuickAction } from "./commands/quickActions.js";
7-
import { setupWorkspace, openPatchloomReleases, openPatchloomSettings } from "./commands/setupWorkspace.js";
7+
import { setupWorkspace, openPatchloomReleases, openPatchloomSettings, openDocumentation } from "./commands/setupWorkspace.js";
88
import { showStatus } from "./commands/showStatus.js";
99
import { verifyMcp } from "./commands/verifyMcp.js";
10+
import { checkForUpdates } from "./commands/autoUpdate.js";
1011
import { setManagedInstallRoot } from "./install/managed.js";
1112
import { createPatchloomLog, getPatchloomLog, setPatchloomLog } from "./logging/outputChannel.js";
13+
import { registerMcpServerProviderWithBinary } from "./mcp/register.js";
1214
import { disposeStatusBar, refreshStatusBar } from "./status/statusBar.js";
1315

1416
export function activate(context: vscode.ExtensionContext): void {
@@ -31,6 +33,7 @@ export function activate(context: vscode.ExtensionContext): void {
3133
vscode.commands.registerCommand("patchloom.updateBinary", updatePatchloom),
3234
vscode.commands.registerCommand("patchloom.reinstallBinary", reinstallPatchloom),
3335
vscode.commands.registerCommand("patchloom.verifyMcp", verifyMcp),
36+
vscode.commands.registerCommand("patchloom.openDocumentation", openDocumentation),
3437
new vscode.Disposable(disposeStatusBar),
3538
vscode.workspace.onDidChangeConfiguration((event) => {
3639
if (event.affectsConfiguration("patchloom")) {
@@ -39,10 +42,15 @@ export function activate(context: vscode.ExtensionContext): void {
3942
}),
4043
vscode.workspace.onDidChangeWorkspaceFolders(() => {
4144
void refreshStatusBar();
45+
}),
46+
vscode.workspace.onDidGrantWorkspaceTrust(() => {
47+
void refreshStatusBar();
4248
})
4349
);
4450

4551
void refreshStatusBar();
52+
void checkForUpdates();
53+
void registerMcpServerProviderWithBinary(context);
4654
}
4755

4856
export function deactivate(): void {

0 commit comments

Comments
 (0)