Skip to content

Commit ea7fcfc

Browse files
authored
feat: managed install fix + 10 competitive gap improvements (#113)
* 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> * fix: inject version in checksum mismatch test to avoid GitHub API rate limits The performManagedInstall checksum mismatch test did not specify a version or fetchLatestVersion mock, causing it to call the real GitHub releases API. On CI runners with shared IPs, this hits 403 rate limits and the test fails with an unrelated error instead of reaching the checksum verification step. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> --------- Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent eda9e6b commit ea7fcfc

16 files changed

Lines changed: 431 additions & 101 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)