diff --git a/AGENTS.md b/AGENTS.md index a366633..3c617f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,13 +51,13 @@ test/ batchApply.test.ts Batch template and operation count parsing (16 tests) binary.test.ts Binary discovery, managed install, compatibility, workspace env (66 tests) binaryDiscovery.test.ts Real executable discovery on PATH (13 tests) - initializeProject.test.ts Status display, agents file classification, formatError (39 tests) + initializeProject.test.ts Status display, agents file classification, formatError (41 tests) managedLifecycle.test.ts Managed install with real file I/O (22 tests) mcpConfig.test.ts MCP config with real temp directories (12 tests) outputChannel.test.ts Output channel logging wrapper (10 tests) patchloomCli.test.ts Patchloom CLI integration with real binary + managed install e2e MCP (40 tests incl. e2e) propertyBased.test.ts Property-based tests with fast-check (13 tests) - quickActions.test.ts Quick action command building, path containment, patch merge (60 tests) + quickActions.test.ts Quick action command building, path containment, patch merge (62 tests) verifyMcp.test.ts MCP server verify and JSON-RPC response parsing (15 tests) downloadIntegration.test.ts HTTP download, redirect, streaming SHA-256 (9 tests) suite/ diff --git a/README.md b/README.md index dc4e956..14d6048 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Run `Patchloom: Setup Workspace` to walk through everything your project needs: ### Agent rules generation -`Patchloom: Initialize Project` generates an `AGENTS.md` file from `patchloom agent-rules`. You pick integration mode (CLI/MCP/all), shell platform, and surface (`full` document or `core` pack for system-prompt injection, CLI 0.24+). If `AGENTS.md` already exists, the extension opens a diff so you can merge updates manually. +`Patchloom: Initialize Project` generates an `AGENTS.md` file from `patchloom agent-rules`. You pick integration mode (CLI/MCP/all), shell platform, and surface (`full` document or `core` pack for system-prompt injection, CLI 0.24+). On CLI 0.29+, `--surface core` honors `--mode` the same way the full surface does. If `AGENTS.md` already exists, the extension opens a diff so you can merge updates manually. ### MCP server configuration @@ -65,7 +65,7 @@ Run `Patchloom: Setup Workspace` to walk through everything your project needs: When configuring, pick **Full tool inventory** (default) or **Core pack**. Core sets `PATCHLOOM_MCP_SURFACE=core` on the server entry. -CLI **0.28.0** (and 0.24+) exposes **58** MCP tools by default (including `list_files` and `apply_fragment`). The core pack is 11 tools: `read_file`, `search_files`, `list_files`, `replace_text`, `batch_replace`, `doc_get`, `doc_set`, `doc_query`, `md_replace_section`, `execute_plan`, `server_info`. Absolute paths that resolve inside the MCP workspace root are allowed; empty paths, `../`, and outside paths still reject with stable `error_kind` peels. +CLI **0.31.0** (and 0.24+) exposes **58** MCP tools by default (including `list_files` and `apply_fragment`). The core pack is 11 tools: `read_file`, `search_files`, `list_files`, `replace_text`, `batch_replace`, `doc_get`, `doc_set`, `doc_query`, `md_replace_section`, `execute_plan`, `server_info`. `search_files` accepts `files_without_match` (CLI 0.29+). `apply_patch` accepts unified diffs, Codex `*** Begin Patch`, and Aider SEARCH/REPLACE (CLI 0.30+). Absolute paths that resolve inside the MCP workspace root are allowed; empty paths, `../`, and outside paths still reject with stable `error_kind` peels. ### Status bar @@ -94,6 +94,7 @@ Click it to see full diagnostics, including per-editor MCP configuration status | **Tidy file** | Whitespace and newline cleanup with diff preview | | **Set structured value** | Update a JSON, YAML, or TOML key with diff preview | | **Search text** | Find pattern matches across workspace files (results in output channel) | +| **Search files without match** | List files that do not contain the pattern (`search -L`, CLI 0.29+) | | **Create file** | Scaffold a new file with optional content and open it in the editor | | **Append to file** | Append content to an existing file | | **Prepend to file** | Prepend content to the start of an existing file (CLI 0.9+) | @@ -175,7 +176,7 @@ The extension detects outdated CLI builds and warns with upgrade guidance. It re Set `patchloom.path` in settings, or add the CLI to your `PATH`. **CLI compatibility warning / upgrade path** -The extension requires Patchloom **0.3.0** or newer; **0.28.0** is recommended. Which fix to use depends on how the CLI was resolved (status shows Source): +The extension requires Patchloom **0.3.0** or newer; **0.31.0** is recommended. Which fix to use depends on how the CLI was resolved (status shows Source): 1. **Source: managed install** → **Patchloom: Update Patchloom** (checksum-verified GitHub release into extension storage) 2. **Source: PATH** → upgrade that install in place (**Scoop** `scoop update patchloom` on Windows; Homebrew / npm / cargo / the official installer elsewhere). Managed Install will not override a PATH binary. @@ -189,6 +190,21 @@ Quick Actions and Batch Apply pass `--contain` so paths stay inside the open wor **Empty or blank path** On CLI 0.28+, empty, whitespace-only, or format-character-only paths fail early with `error_kind: invalid_input` and message `path must not be empty` (they no longer look like workspace-root failures). Prefer a real relative or workspace-absolute path. +**Create or rename parent is not a directory** +On CLI 0.31+, create and rename check each parent of the destination before staging. A parent that is a regular file (or a dangling / file symlink) reports `error_kind: invalid_input` and `parent path is not a directory`. Create the missing directory first, or pick a destination whose parents are directories. + +**Numeric or negated doc selector** +On CLI 0.30+, selectors accept `!=`, `>`, `>=`, `<`, `<=`, and `[!key]` (for example `servers[port>8000]`). A non-numeric compare reports `error_kind: invalid_input`. Use a number on the right-hand side, or a concrete index path. + +**Search files without match** +On CLI 0.29+, `search -L` / `--files-without-match` lists files that do not contain the pattern. Combining it with `--files-with-matches` or `--count` is `invalid_input`. When every scanned file contains the pattern, the CLI reports `error_kind: no_matches` and the text `no files without matches for 'PATTERN' in SCOPE` (that is not a content miss). + +**YAML mapping alias stayed an alias** +On CLI 0.31+, `doc set` on a mapping that is only `service_a: *shared` writes a merge key plus local fields (`<<: *shared` and your new keys) instead of inlining the whole object. Sequence items (`- *shared`) still expand or stay not-applied. + +**Patch apply formats** +On CLI 0.30+, `patch apply` (and MCP `apply_patch`) accepts unified diffs, Codex `*** Begin Patch`, and Aider SEARCH/REPLACE. Update and SEARCH matches must be unique unless you pass `--replace-all` (SEARCH/REPLACE only). The Quick Action **Merge patch (three-way)** is still `patch merge` for stale unified diffs. + **Batch replace shape** Batch lines use `replace PATH OLD NEW` (and optional flags such as `--fuzzy`). Do not paste CLI form `replace OLD --new NEW path` into a batch plan; CLI 0.18+ returns a clear parse error with the PATH OLD NEW hint. @@ -240,7 +256,7 @@ File bugs and feature requests at [patchloom/patchloom-vscode/issues](https://gi ## Requirements - VS Code 1.90 or newer (or compatible editors: Cursor, Windsurf, VSCodium) -- [Patchloom CLI](https://github.com/patchloom/patchloom) 0.3.0 or newer (**0.28.0+ recommended** for empty-path fail-closed (`path must not be empty`), `suggested_op` on fail-closed doc navigation, `not_regular_file` soft peels, ambiguous markdown headings, `list_files` MCP inventory, `apply-fragment`, full `error_kind` peels (`binary` / `invalid_encoding` / `fuzzy_span_suspicious` / `already_exists` / `guard_rejected` / `ambiguous`), optional `PATCHLOOM_MCP_SURFACE=core` 11-tool pack, multi-doc `doc merge --selector`, line-oriented inserts, batch `replace PATH OLD NEW` hints, 58 MCP tools, and agent-facing JSON envelopes) +- [Patchloom CLI](https://github.com/patchloom/patchloom) 0.3.0 or newer (**0.31.0+ recommended** for YAML alias-to-merge on `doc set`, create/rename `parent path is not a directory`, numeric selector compares (`servers[port>8000]`), `search -L` / `files_without_match`, Codex Begin Patch and Aider SEARCH/REPLACE on `patch apply`, `agent-rules --surface core` honoring `--mode`, empty-path fail-closed (`path must not be empty`), `suggested_op` on fail-closed doc navigation, `not_regular_file` soft peels, ambiguous markdown headings, `list_files` MCP inventory, `apply-fragment`, full `error_kind` peels (`binary` / `invalid_encoding` / `fuzzy_span_suspicious` / `already_exists` / `guard_rejected` / `ambiguous`), optional `PATCHLOOM_MCP_SURFACE=core` 11-tool pack, multi-doc `doc merge --selector`, line-oriented inserts, batch `replace PATH OLD NEW` hints, 58 MCP tools, and agent-facing JSON envelopes) ## Contributing diff --git a/src/commands/configureMcp.ts b/src/commands/configureMcp.ts index c89b75d..1016b3f 100644 --- a/src/commands/configureMcp.ts +++ b/src/commands/configureMcp.ts @@ -47,7 +47,7 @@ export async function configureMcp(): Promise { [ { label: "Full tool inventory", - description: "Default (58 tools on CLI 0.24+; verified on 0.28)", + description: "Default (58 tools on CLI 0.24+; verified on 0.31)", surface: "full" as const }, { diff --git a/src/commands/initializeProject.ts b/src/commands/initializeProject.ts index c40129a..789e974 100644 --- a/src/commands/initializeProject.ts +++ b/src/commands/initializeProject.ts @@ -62,7 +62,7 @@ export async function initializeProject(): Promise { }, { label: "Core pack", - description: "Short rules for system-prompt injection (CLI 0.24+ --surface core)", + description: "Short rules for system-prompt injection (CLI 0.24+ --surface core; 0.29+ honors --mode)", surface: "core" as const } ], diff --git a/src/commands/quickActions.ts b/src/commands/quickActions.ts index 31a6d4d..30aee90 100644 --- a/src/commands/quickActions.ts +++ b/src/commands/quickActions.ts @@ -259,8 +259,8 @@ export async function runQuickAction(): Promise { } const selector = await vscode.window.showInputBox({ - prompt: "Selector path", - placeHolder: "scripts.test", + prompt: "Selector path (CLI 0.30+ accepts numeric compares such as servers[port>8000])", + placeHolder: "scripts.test or servers[port>8000]", validateInput: (value) => value.length > 0 ? undefined : "Selector is required." }); if (selector === undefined) { @@ -325,6 +325,55 @@ export async function runQuickAction(): Promise { } } }, + { + label: "Search files without match", + description: "List files that do not contain the pattern (CLI 0.29+ -L)", + detail: "Builds `patchloom search --files-without-match [--glob ] `", + run: async () => { + const folder = await activeWorkspaceFolder({ + promptIfMany: true, + placeHolder: "Select workspace folder for Patchloom search -L" + }); + if (!folder) { + await vscode.window.showWarningMessage("Open a workspace folder before running Patchloom search."); + return; + } + + const pattern = await vscode.window.showInputBox({ + prompt: "Pattern that listed files must not contain", + placeHolder: "TODO|FIXME", + validateInput: (value) => value.length > 0 ? undefined : "Pattern is required." + }); + if (pattern === undefined) { + return; + } + + const glob = await vscode.window.showInputBox({ + prompt: "File glob (optional, leave empty for all files)", + placeHolder: "*.ts" + }); + if (glob === undefined) { + return; + } + + const action = buildSearchQuickAction(folder.uri.fsPath, pattern, glob || undefined, { + filesWithoutMatch: true + }); + const result = await executePatchloom(binaryPath, action.args, folder.uri.fsPath); + const log = getPatchloomLog(); + + if (result.exitCode === 3) { + await vscode.window.showInformationMessage(`Every scanned file contains "${pattern}".`); + } else if (result.exitCode !== 0) { + await vscode.window.showErrorMessage(`Patchloom search failed: ${formatCliOutput(result)}`); + } else { + log?.show(); + await vscode.window.showInformationMessage( + "Files without matches are listed in the Patchloom output channel." + ); + } + } + }, { label: "Create a new file", description: "Scaffold a new file in the workspace", @@ -440,8 +489,8 @@ export async function runQuickAction(): Promise { } const selector = await vscode.window.showInputBox({ - prompt: "Selector path", - placeHolder: "scripts.test", + prompt: "Selector path (CLI 0.30+ accepts numeric compares such as servers[port>8000])", + placeHolder: "scripts.test or servers[port>8000]", validateInput: (value) => value.length > 0 ? undefined : "Selector is required." }); if (selector === undefined) { @@ -1097,8 +1146,16 @@ export function buildDocSetQuickAction(targetPath: string, selector: string, val }; } -export function buildSearchQuickAction(workspacePath: string, pattern: string, glob?: string): PlannedQuickAction { +export function buildSearchQuickAction( + workspacePath: string, + pattern: string, + glob?: string, + options?: { readonly filesWithoutMatch?: boolean } +): PlannedQuickAction { const args: string[] = ["search", pattern]; + if (options?.filesWithoutMatch) { + args.push("--files-without-match"); + } if (glob) { args.push("--glob", glob); } @@ -1106,7 +1163,9 @@ export function buildSearchQuickAction(workspacePath: string, pattern: string, g const targetIndex = args.length - 1; return { - title: `Search for "${pattern}"`, + title: options?.filesWithoutMatch + ? `Search files without "${pattern}"` + : `Search for "${pattern}"`, targetPath: workspacePath, targetArgIndices: [targetIndex], args diff --git a/test/unit/binary.test.ts b/test/unit/binary.test.ts index d4be303..a53a155 100644 --- a/test/unit/binary.test.ts +++ b/test/unit/binary.test.ts @@ -276,8 +276,8 @@ test("preferredBinaryRemediationAction is undefined when ready and supported", ( source: "path", message: "Using Patchloom from PATH.", binaryPath: "/usr/local/bin/patchloom", - version: "patchloom 0.28.0", - detectedVersion: "0.28.0", + version: "patchloom 0.31.0", + detectedVersion: "0.31.0", compatibility: "supported", minimumSupportedVersion: MINIMUM_SUPPORTED_PATCHLOOM_VERSION }); diff --git a/test/unit/initializeProject.test.ts b/test/unit/initializeProject.test.ts index 2c6e4e9..c1907d9 100644 --- a/test/unit/initializeProject.test.ts +++ b/test/unit/initializeProject.test.ts @@ -150,6 +150,32 @@ test("formatCliOutput surfaces empty path invalid_input (CLI 0.28+)", () => { ); }); +test("formatCliOutput surfaces parent path invalid_input (CLI 0.31+)", () => { + const stdout = JSON.stringify({ + ok: false, + error: "parent path is not a directory: /tmp/notdir", + error_kind: "invalid_input", + applied: false + }); + assert.equal( + formatCliOutput({ exitCode: 1, stdout, stderr: "" }), + "invalid_input: parent path is not a directory: /tmp/notdir" + ); +}); + +test("formatCliOutput surfaces numeric selector invalid_input (CLI 0.30+)", () => { + const stdout = JSON.stringify({ + ok: false, + error: "selector error: comparison operand must be numeric (got 'abc' after >)", + error_kind: "invalid_input", + applied: false + }); + assert.equal( + formatCliOutput({ exitCode: 1, stdout, stderr: "" }), + "invalid_input: selector error: comparison operand must be numeric (got 'abc' after >)" + ); +}); + test("formatCliOutput appends suggested_op when present (CLI 0.27+)", () => { const stdout = JSON.stringify({ ok: false, diff --git a/test/unit/patchloomCli.test.ts b/test/unit/patchloomCli.test.ts index 1c320a8..24b9751 100644 --- a/test/unit/patchloomCli.test.ts +++ b/test/unit/patchloomCli.test.ts @@ -29,6 +29,7 @@ import { buildApplyFragmentQuickAction, buildInsertAfterMatchQuickAction, buildReplaceQuickAction, + buildSearchQuickAction, retargetQuickAction, withApplyFlag } from "../../src/commands/quickActions.js"; @@ -581,6 +582,56 @@ describe("patchloom CLI integration", async () => { }); }); + test("search --files-without-match via Quick Action args (CLI 0.29+)", async (t) => { + const { stdout, stderr } = await execFileAsync(binaryPath, ["--version"], { timeout: 5000 }); + const version = parsePatchloomVersion(`${stdout}${stderr}`); + if (!version || comparePatchloomVersions(version, "0.29.0") < 0) { + t.skip(`requires patchloom >= 0.29.0 (found ${version ?? "unknown"})`); + return; + } + + await withTempDir(async (dir) => { + await fs.writeFile(path.join(dir, "hit.txt"), "TODO\n", "utf8"); + await fs.writeFile(path.join(dir, "miss.txt"), "ok\n", "utf8"); + + const action = buildSearchQuickAction(dir, "TODO", undefined, { filesWithoutMatch: true }); + const result = await execFileAsync(binaryPath, ["--json", ...action.args], { timeout: 5000 }); + const parsed = JSON.parse(result.stdout) as { + files?: Array<{ path?: string }>; + }; + const files = (parsed.files ?? []).map((entry) => path.basename(entry.path ?? "")); + assert.ok(files.includes("miss.txt"), "files without the pattern should be listed"); + assert.ok(!files.includes("hit.txt"), "files that contain the pattern should be omitted"); + }); + }); + + test("create through a file parent is invalid_input (CLI 0.31+)", async (t) => { + const { stdout, stderr } = await execFileAsync(binaryPath, ["--version"], { timeout: 5000 }); + const version = parsePatchloomVersion(`${stdout}${stderr}`); + if (!version || comparePatchloomVersions(version, "0.31.0") < 0) { + t.skip(`requires patchloom >= 0.31.0 (found ${version ?? "unknown"})`); + return; + } + + await withTempDir(async (dir) => { + const parent = path.join(dir, "notdir"); + await fs.writeFile(parent, "file\n", "utf8"); + const dest = path.join(parent, "child.txt"); + + try { + await execFileAsync(binaryPath, ["--json", "create", dest, "--content", "x", "--apply"], { + timeout: 5000 + }); + assert.fail("create through a file parent should fail"); + } catch (error) { + const failed = error as { stdout?: string; stderr?: string }; + const payload = `${failed.stdout ?? ""}${failed.stderr ?? ""}`; + assert.match(payload, /parent path is not a directory/); + assert.match(payload, /invalid_input/); + } + }); + }); + test("doc append adds an item to a JSON array", async () => { await withTempDir(async (dir) => { const file = path.join(dir, "config.json"); diff --git a/test/unit/quickActions.test.ts b/test/unit/quickActions.test.ts index fd3dfe1..56d0bee 100644 --- a/test/unit/quickActions.test.ts +++ b/test/unit/quickActions.test.ts @@ -266,6 +266,28 @@ test("buildSearchQuickAction includes glob when provided", () => { assert.deepEqual(action.targetArgIndices, [4]); }); +test("buildSearchQuickAction includes --files-without-match (CLI 0.29+)", () => { + const action = buildSearchQuickAction("/workspace/demo", "TODO", undefined, { + filesWithoutMatch: true + }); + + assert.equal(action.title, 'Search files without "TODO"'); + assert.deepEqual(action.args, ["search", "TODO", "--files-without-match", "/workspace/demo"]); + assert.deepEqual(action.targetArgIndices, [3]); +}); + +test("buildSearchQuickAction combines --files-without-match with glob", () => { + const action = buildSearchQuickAction("/workspace/demo", "TODO", "*.ts", { + filesWithoutMatch: true + }); + + assert.equal(action.title, 'Search files without "TODO"'); + assert.deepEqual(action.args, [ + "search", "TODO", "--files-without-match", "--glob", "*.ts", "/workspace/demo" + ]); + assert.deepEqual(action.targetArgIndices, [5]); +}); + test("buildCreateQuickAction builds a create command with content and apply", () => { const action = buildCreateQuickAction("/workspace/demo/src/newfile.ts", "hello"); diff --git a/walkthrough/initialize.md b/walkthrough/initialize.md index c1d952c..0c02748 100644 --- a/walkthrough/initialize.md +++ b/walkthrough/initialize.md @@ -6,7 +6,8 @@ your codebase. Click **Initialize Project** above to run `patchloom agent-rules` in your workspace. You can choose integration mode (CLI + MCP, CLI only, or MCP only), shell platform examples (all, Linux/macOS, or Windows), and -surface (full document or core pack for system-prompt injection). +surface (full document or core pack for system-prompt injection). On +CLI 0.29+, the core pack text follows the mode you pick. ## What AGENTS.md Contains