Skip to content

Commit d272511

Browse files
committed
feat: implement managed Patchloom binary install, update, and reinstall
Add the full managed binary lifecycle so the extension can install and maintain Patchloom without requiring a pre-existing local install. Core I/O functions in managed.ts: - fetchLatestReleaseVersion: query GitHub Releases API for latest tag - downloadToFile: streaming HTTPS download with redirect following - extractManagedInstallArchive: shell out to tar for all platforms - performManagedInstall: orchestrate download, verify, extract, promote VS Code commands (managedInstall.ts): - Install Patchloom: detect platform, fetch latest, install with progress - Update Patchloom: compare versions, confirm upgrade, install - Reinstall Patchloom: force reinstall for recovery from corruption Integration: - Register 3 commands in extension.ts and package.json - Status bar suggests Install Patchloom when binary is missing - All functions use dependency injection for testability Tests (11 new, 177 total): - fetchLatestReleaseVersion with injected mock - extractManagedInstallArchive argument verification - performManagedInstall full pipeline, checksum mismatch, unsupported platform, and auto-version-fetch scenarios - preferredStatusAction install suggestion Closes #9 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent cf8512d commit d272511

8 files changed

Lines changed: 705 additions & 2 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ src/
3131
commands/
3232
configureMcp.ts Configure MCP command: multi-target MCP config injection
3333
initializeProject.ts Initialize Project command: generate/diff AGENTS.md
34+
managedInstall.ts Managed Install commands: install, update, reinstall Patchloom binary
3435
quickActions.ts Quick Action command: replace, tidy, doc set, search, create, doc get
3536
batchApply.ts Batch Apply command: atomic multi-operation plan via JSON
3637
setupWorkspace.ts Setup Workspace command: guided readiness walkthrough

package.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
"onCommand:patchloom.openPatchloomReleases",
5151
"onCommand:patchloom.showStatus",
5252
"onCommand:patchloom.batchApply",
53-
"onCommand:patchloom.showOutput"
53+
"onCommand:patchloom.showOutput",
54+
"onCommand:patchloom.installBinary",
55+
"onCommand:patchloom.updateBinary",
56+
"onCommand:patchloom.reinstallBinary"
5457
],
5558
"contributes": {
5659
"commands": [
@@ -98,6 +101,21 @@
98101
"command": "patchloom.showOutput",
99102
"title": "Show Output",
100103
"category": "Patchloom"
104+
},
105+
{
106+
"command": "patchloom.installBinary",
107+
"title": "Install Patchloom",
108+
"category": "Patchloom"
109+
},
110+
{
111+
"command": "patchloom.updateBinary",
112+
"title": "Update Patchloom",
113+
"category": "Patchloom"
114+
},
115+
{
116+
"command": "patchloom.reinstallBinary",
117+
"title": "Reinstall Patchloom",
118+
"category": "Patchloom"
101119
}
102120
],
103121
"configuration": {

src/commands/managedInstall.ts

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import * as vscode from "vscode";
2+
import { resolvePatchloomStatus, comparePatchloomVersions, PATCHLOOM_RELEASES_URL } from "../binary/patchloom.js";
3+
import {
4+
detectManagedInstallTarget,
5+
fetchLatestReleaseVersion,
6+
getManagedInstallRoot,
7+
ManagedInstallStage,
8+
performManagedInstall
9+
} from "../install/managed.js";
10+
import { getPatchloomLog } from "../logging/outputChannel.js";
11+
import { refreshStatusBar } from "../status/statusBar.js";
12+
13+
function stageLabel(stage: ManagedInstallStage): string {
14+
switch (stage) {
15+
case "fetching-version":
16+
return "Checking for latest release...";
17+
case "downloading-checksum":
18+
return "Downloading checksum...";
19+
case "downloading-archive":
20+
return "Downloading Patchloom...";
21+
case "verifying":
22+
return "Verifying integrity...";
23+
case "extracting":
24+
return "Extracting archive...";
25+
case "installing":
26+
return "Installing binary...";
27+
}
28+
}
29+
30+
export async function installPatchloom(): Promise<void> {
31+
const installRoot = getManagedInstallRoot();
32+
if (!installRoot) {
33+
await vscode.window.showErrorMessage("Managed install is not available: extension storage path is not set.");
34+
return;
35+
}
36+
37+
const target = detectManagedInstallTarget();
38+
if (!target) {
39+
await vscode.window.showErrorMessage(
40+
`Managed install is not supported on this platform (${process.platform}/${process.arch}).`
41+
);
42+
return;
43+
}
44+
45+
const log = getPatchloomLog();
46+
47+
await vscode.window.withProgress(
48+
{
49+
location: vscode.ProgressLocation.Notification,
50+
title: "Patchloom",
51+
cancellable: false
52+
},
53+
async (progress) => {
54+
try {
55+
const result = await performManagedInstall({
56+
installRoot,
57+
onProgress: (stage) => {
58+
progress.report({ message: stageLabel(stage) });
59+
}
60+
});
61+
62+
log?.log(`Managed install complete: Patchloom ${result.version} at ${result.binaryPath}`);
63+
await refreshStatusBar();
64+
await vscode.window.showInformationMessage(
65+
`Patchloom ${result.version} installed successfully.`
66+
);
67+
} catch (error) {
68+
const message = error instanceof Error ? error.message : String(error);
69+
log?.log(`Managed install failed: ${message}`);
70+
await vscode.window.showErrorMessage(`Failed to install Patchloom: ${message}`);
71+
}
72+
}
73+
);
74+
}
75+
76+
export async function updatePatchloom(): Promise<void> {
77+
const installRoot = getManagedInstallRoot();
78+
if (!installRoot) {
79+
await vscode.window.showErrorMessage("Managed install is not available: extension storage path is not set.");
80+
return;
81+
}
82+
83+
const target = detectManagedInstallTarget();
84+
if (!target) {
85+
await vscode.window.showErrorMessage(
86+
`Managed install is not supported on this platform (${process.platform}/${process.arch}).`
87+
);
88+
return;
89+
}
90+
91+
const log = getPatchloomLog();
92+
93+
await vscode.window.withProgress(
94+
{
95+
location: vscode.ProgressLocation.Notification,
96+
title: "Patchloom",
97+
cancellable: false
98+
},
99+
async (progress) => {
100+
try {
101+
progress.report({ message: "Checking for updates..." });
102+
const latestVersion = await fetchLatestReleaseVersion();
103+
104+
const status = await resolvePatchloomStatus();
105+
if (status.detectedVersion && comparePatchloomVersions(latestVersion, status.detectedVersion) <= 0) {
106+
await vscode.window.showInformationMessage(
107+
`Patchloom ${status.detectedVersion} is already up to date.`
108+
);
109+
return;
110+
}
111+
112+
const upgradeLabel = status.detectedVersion
113+
? `${status.detectedVersion} to ${latestVersion}`
114+
: latestVersion;
115+
116+
const choice = await vscode.window.showInformationMessage(
117+
`Patchloom ${upgradeLabel} is available. Install now?`,
118+
"Install",
119+
"View Release"
120+
);
121+
122+
if (choice === "View Release") {
123+
await vscode.env.openExternal(
124+
vscode.Uri.parse(`${PATCHLOOM_RELEASES_URL}/tag/v${latestVersion}`)
125+
);
126+
return;
127+
}
128+
129+
if (choice !== "Install") {
130+
return;
131+
}
132+
133+
const result = await performManagedInstall({
134+
installRoot,
135+
version: latestVersion,
136+
onProgress: (stage) => {
137+
progress.report({ message: stageLabel(stage) });
138+
}
139+
});
140+
141+
log?.log(`Managed update complete: Patchloom ${result.version} at ${result.binaryPath}`);
142+
await refreshStatusBar();
143+
await vscode.window.showInformationMessage(
144+
`Patchloom updated to ${result.version}.`
145+
);
146+
} catch (error) {
147+
const message = error instanceof Error ? error.message : String(error);
148+
log?.log(`Managed update failed: ${message}`);
149+
await vscode.window.showErrorMessage(`Failed to update Patchloom: ${message}`);
150+
}
151+
}
152+
);
153+
}
154+
155+
export async function reinstallPatchloom(): Promise<void> {
156+
const installRoot = getManagedInstallRoot();
157+
if (!installRoot) {
158+
await vscode.window.showErrorMessage("Managed install is not available: extension storage path is not set.");
159+
return;
160+
}
161+
162+
const target = detectManagedInstallTarget();
163+
if (!target) {
164+
await vscode.window.showErrorMessage(
165+
`Managed install is not supported on this platform (${process.platform}/${process.arch}).`
166+
);
167+
return;
168+
}
169+
170+
const choice = await vscode.window.showWarningMessage(
171+
"This will re-download and reinstall the Patchloom binary. Continue?",
172+
"Reinstall",
173+
"Cancel"
174+
);
175+
176+
if (choice !== "Reinstall") {
177+
return;
178+
}
179+
180+
const log = getPatchloomLog();
181+
182+
await vscode.window.withProgress(
183+
{
184+
location: vscode.ProgressLocation.Notification,
185+
title: "Patchloom",
186+
cancellable: false
187+
},
188+
async (progress) => {
189+
try {
190+
const result = await performManagedInstall({
191+
installRoot,
192+
onProgress: (stage) => {
193+
progress.report({ message: stageLabel(stage) });
194+
}
195+
});
196+
197+
log?.log(`Managed reinstall complete: Patchloom ${result.version} at ${result.binaryPath}`);
198+
await refreshStatusBar();
199+
await vscode.window.showInformationMessage(
200+
`Patchloom ${result.version} reinstalled successfully.`
201+
);
202+
} catch (error) {
203+
const message = error instanceof Error ? error.message : String(error);
204+
log?.log(`Managed reinstall failed: ${message}`);
205+
await vscode.window.showErrorMessage(`Failed to reinstall Patchloom: ${message}`);
206+
}
207+
}
208+
);
209+
}

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from "vscode";
22
import { batchApply } from "./commands/batchApply.js";
33
import { configureMcp } from "./commands/configureMcp.js";
44
import { initializeProject } from "./commands/initializeProject.js";
5+
import { installPatchloom, updatePatchloom, reinstallPatchloom } from "./commands/managedInstall.js";
56
import { runQuickAction } from "./commands/quickActions.js";
67
import { setupWorkspace, openPatchloomReleases, openPatchloomSettings } from "./commands/setupWorkspace.js";
78
import { showStatus } from "./commands/showStatus.js";
@@ -25,6 +26,9 @@ export function activate(context: vscode.ExtensionContext): void {
2526
vscode.commands.registerCommand("patchloom.openPatchloomSettings", openPatchloomSettings),
2627
vscode.commands.registerCommand("patchloom.openPatchloomReleases", openPatchloomReleases),
2728
vscode.commands.registerCommand("patchloom.showStatus", showStatus),
29+
vscode.commands.registerCommand("patchloom.installBinary", installPatchloom),
30+
vscode.commands.registerCommand("patchloom.updateBinary", updatePatchloom),
31+
vscode.commands.registerCommand("patchloom.reinstallBinary", reinstallPatchloom),
2832
new vscode.Disposable(disposeStatusBar),
2933
vscode.workspace.onDidChangeConfiguration((event) => {
3034
if (event.affectsConfiguration("patchloom")) {

0 commit comments

Comments
 (0)