Skip to content

Commit e451e03

Browse files
committed
feat: add output channel, search/create/doc-get actions, and batch apply
- Output channel (#27): PatchloomLog wrapper with lazy channel creation, logCommand/logResult integration, Show Output command, DI-friendly - Quick actions (#28): search (output channel results), create (opens new file), doc get (clipboard + info message) added to picker - Batch apply (#29): opens JSON template, pipes plan via stdin to patchloom batch --apply, shows operation count on success - All CLI invocations now logged to the Patchloom output channel - 17 new tests (7 output channel, 6 batch, 4 quick action builders) Closes #27 Closes #28 Closes #29 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent 4434742 commit e451e03

9 files changed

Lines changed: 605 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,27 @@ 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-
quickActions.ts Quick Action command: replace, tidy, doc set with diff preview
34+
quickActions.ts Quick Action command: replace, tidy, doc set, search, create, doc get
35+
batchApply.ts Batch Apply command: atomic multi-operation plan via JSON
3536
setupWorkspace.ts Setup Workspace command: guided readiness walkthrough
3637
showStatus.ts Show Status command: diagnostics display
3738
install/managed.ts Managed install safety: checksum, staging, promotion, rollback, persistence
39+
logging/outputChannel.ts Output channel wrapper: log, logCommand, logResult, show, dispose
3840
mcp/config.ts MCP config file operations: inspect, configure, resolve targets
3941
status/details.ts Status presentation: buildStatusDetails, preferredStatusAction
4042
status/statusBar.ts Status bar item: create, refresh, dispose
4143
workspace/readiness.ts Workspace readiness: environment detection, folder selection
4244
test/
4345
unit/ Unit tests (node:test, dependency-injected, no VS Code API)
46+
batchApply.test.ts Batch template and operation count parsing (6 tests)
4447
binary.test.ts Binary discovery, managed install, compatibility, workspace env (38 tests)
4548
binaryDiscovery.test.ts Real executable discovery on PATH (10 tests)
4649
initializeProject.test.ts Status display, agents file classification, formatError (18 tests)
4750
managedLifecycle.test.ts Managed install with real file I/O (12 tests)
4851
mcpConfig.test.ts MCP config with real temp directories (9 tests)
52+
outputChannel.test.ts Output channel logging wrapper (7 tests)
4953
patchloomCli.test.ts Patchloom CLI integration tests with real binary (23 tests)
50-
quickActions.test.ts Quick action command building (10 tests)
54+
quickActions.test.ts Quick action command building (14 tests)
5155
suite/
5256
index.ts VS Code extension integration tests
5357
runExtensionTests.ts Test runner using @vscode/test-electron

package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
"onCommand:patchloom.quickAction",
4949
"onCommand:patchloom.openPatchloomSettings",
5050
"onCommand:patchloom.openPatchloomReleases",
51-
"onCommand:patchloom.showStatus"
51+
"onCommand:patchloom.showStatus",
52+
"onCommand:patchloom.batchApply",
53+
"onCommand:patchloom.showOutput"
5254
],
5355
"contributes": {
5456
"commands": [
@@ -86,6 +88,16 @@
8688
"command": "patchloom.showStatus",
8789
"title": "Show Status",
8890
"category": "Patchloom"
91+
},
92+
{
93+
"command": "patchloom.batchApply",
94+
"title": "Batch Apply",
95+
"category": "Patchloom"
96+
},
97+
{
98+
"command": "patchloom.showOutput",
99+
"title": "Show Output",
100+
"category": "Patchloom"
89101
}
90102
],
91103
"configuration": {

src/commands/batchApply.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { execFile } from "node:child_process";
2+
import type * as VSCode from "vscode";
3+
import { patchloomNeedsUpgrade, resolvePatchloomStatus } from "../binary/patchloom.js";
4+
import { getPatchloomLog } from "../logging/outputChannel.js";
5+
import { formatError } from "../util.js";
6+
import { activeWorkspaceFolder } from "../workspace/readiness.js";
7+
8+
export const BATCH_TEMPLATE = JSON.stringify({
9+
operations: [
10+
{ op: "replace", file: "", from: "", to: "" },
11+
{ op: "tidy", file: "", fixes: ["ensure-final-newline"] },
12+
{ op: "doc-set", file: "", selector: "", value: "" }
13+
]
14+
}, null, 2) + "\n";
15+
16+
export function buildBatchTemplate(): string {
17+
return BATCH_TEMPLATE;
18+
}
19+
20+
export function parseBatchOperationCount(plan: string): number {
21+
try {
22+
const parsed = JSON.parse(plan);
23+
if (Array.isArray(parsed?.operations)) {
24+
return parsed.operations.length;
25+
}
26+
} catch {
27+
// Invalid JSON
28+
}
29+
return 0;
30+
}
31+
32+
export async function batchApply(): Promise<void> {
33+
const vscode: typeof VSCode = await import("vscode");
34+
const status = await resolvePatchloomStatus();
35+
if (!status.ready || !status.binaryPath) {
36+
const choice = await vscode.window.showWarningMessage(status.message, "Open Settings");
37+
if (choice === "Open Settings") {
38+
await vscode.commands.executeCommand("patchloom.openPatchloomSettings");
39+
}
40+
return;
41+
}
42+
43+
if (patchloomNeedsUpgrade(status)) {
44+
const choice = await vscode.window.showWarningMessage(
45+
`${status.compatibilityMessage}\n\nUpgrade Patchloom before running batch operations.`,
46+
"Open Releases"
47+
);
48+
if (choice === "Open Releases") {
49+
await vscode.commands.executeCommand("patchloom.openPatchloomReleases");
50+
}
51+
return;
52+
}
53+
54+
const folder = await activeWorkspaceFolder({
55+
promptIfMany: true,
56+
placeHolder: "Select workspace folder for batch apply"
57+
});
58+
if (!folder) {
59+
await vscode.window.showWarningMessage("Open a workspace folder before running Patchloom: Batch Apply.");
60+
return;
61+
}
62+
63+
const binaryPath = status.binaryPath;
64+
const doc = await vscode.workspace.openTextDocument({
65+
language: "json",
66+
content: BATCH_TEMPLATE
67+
});
68+
await vscode.window.showTextDocument(doc, { preview: false });
69+
70+
const choice = await vscode.window.showInformationMessage(
71+
"Edit the batch plan, then click Apply to execute all operations atomically.",
72+
"Apply"
73+
);
74+
if (choice !== "Apply") {
75+
return;
76+
}
77+
78+
const plan = doc.getText();
79+
const log = getPatchloomLog();
80+
const args = ["batch", "--apply"];
81+
log?.logCommand(binaryPath, args, folder.uri.fsPath);
82+
83+
const result = await executePatchloomWithStdin(binaryPath, args, folder.uri.fsPath, plan);
84+
log?.logResult(result.exitCode, result.stdout, result.stderr);
85+
86+
if (result.exitCode !== 0) {
87+
log?.show();
88+
await vscode.window.showErrorMessage(
89+
`Batch apply failed: ${formatBatchOutput(result)}`
90+
);
91+
return;
92+
}
93+
94+
const ops = parseBatchOperationCount(plan);
95+
log?.show();
96+
await vscode.window.showInformationMessage(
97+
`Batch apply completed: ${ops} operation(s) applied.`
98+
);
99+
}
100+
101+
interface BatchCommandResult {
102+
readonly exitCode: number;
103+
readonly stdout: string;
104+
readonly stderr: string;
105+
}
106+
107+
function executePatchloomWithStdin(
108+
binaryPath: string,
109+
args: readonly string[],
110+
cwd: string,
111+
stdin: string
112+
): Promise<BatchCommandResult> {
113+
return new Promise((resolve) => {
114+
const child = execFile(binaryPath, [...args], {
115+
cwd,
116+
timeout: 30_000,
117+
maxBuffer: 8 * 1024 * 1024,
118+
windowsHide: true
119+
}, (error, stdout, stderr) => {
120+
if (error) {
121+
resolve({
122+
exitCode: typeof error.code === "number" ? error.code : 1,
123+
stdout,
124+
stderr: stderr || error.message
125+
});
126+
} else {
127+
resolve({ exitCode: 0, stdout, stderr });
128+
}
129+
});
130+
131+
if (child.stdin) {
132+
child.stdin.write(stdin);
133+
child.stdin.end();
134+
}
135+
});
136+
}
137+
138+
function formatBatchOutput(result: BatchCommandResult): string {
139+
const output = `${result.stderr}\n${result.stdout}`
140+
.split(/\r?\n/)
141+
.map((line) => line.trim())
142+
.filter((line) => line.length > 0)
143+
.join(" ");
144+
return output || `exit code ${result.exitCode}`;
145+
}

0 commit comments

Comments
 (0)