diff --git a/CHANGELOG.md b/CHANGELOG.md index 169afa3..f911098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project are documented here, following [Keep a Changelog](https://keepachangelog.com/) and semantic versioning. +## [0.1.6] - 2026-09-08 + +### Fixed + +- Write and report each ignore file only once when `--targets` contains duplicates. + ## [0.1.5] - 2026-09-07 ### Fixed diff --git a/README.md b/README.md index e3f364b..93cc266 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ npx ctxtrim --fail-on-waste 40 # CI: exit 1 if 40%+ of context is junk **Ignore files it writes** (all real formats the tools honor): `.cursorignore` (Cursor), `.aiexclude` (Gemini Code Assist / Firebase Studio), `.aiignore` (generic). Writes are **idempotent** — a managed block between markers, so your own rules are preserved and re-runs just update the block. +Repeated names in `--targets` are ignored; each ignore file is written and reported once, in the order first requested. + ## What it is (and isn't) - A **fast, deterministic, zero-dependency** heuristic — no model calls, nothing uploaded. diff --git a/package-lock.json b/package-lock.json index c70f8c6..f9fd278 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ctxtrim", - "version": "0.1.4", + "version": "0.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctxtrim", - "version": "0.1.4", + "version": "0.1.6", "license": "MIT", "bin": { "ctxtrim": "bin/ctxtrim.js" diff --git a/package.json b/package.json index e5d9185..0c68064 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctxtrim", - "version": "0.1.5", + "version": "0.1.6", "description": "Trim what bloats your AI coding context. Scan a repo, find the high-cost/low-value files ballooning your Claude Code / Cursor / Codex context, and write ignore files to cut token cost. Zero dependencies.", "type": "module", "bin": { diff --git a/src/ignore.js b/src/ignore.js index e6b7e01..5d3b89a 100644 --- a/src/ignore.js +++ b/src/ignore.js @@ -37,7 +37,7 @@ export function merge(existing, patterns) { /** @returns {{file, action:'created'|'updated', patterns:number}[]} */ export function writeIgnores(root, patterns, targets) { const results = []; - for (const t of targets) { + for (const t of new Set(targets)) { const fname = TARGETS[t]; if (!fname) continue; const path = join(root, fname); diff --git a/test/ctxtrim.test.js b/test/ctxtrim.test.js index 72edc54..cd55a34 100644 --- a/test/ctxtrim.test.js +++ b/test/ctxtrim.test.js @@ -210,6 +210,23 @@ test("unknown --targets values fail before writing ignore files", (t) => { assert.equal(existsSync(join(root, ".aiexclude")), false); assert.equal(existsSync(join(root, ".aiignore")), false); }); +test("duplicate --targets writes and reports each ignore file once", (t) => { + const root = mkdtempSync(join(tmpdir(), "ctxtrim-duplicate-target-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + writeFileSync(join(root, "package-lock.json"), "{}\n"); + + const result = spawnSync(process.execPath, [ + cli, root, "--write", "--format", "json", "--targets", "cursor, gemini,cursor,gemini", + ], { encoding: "utf8" }); + + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(JSON.parse(result.stdout).wrote, [ + { file: ".cursorignore", action: "created", patterns: 1 }, + { file: ".aiexclude", action: "created", patterns: 1 }, + ]); + assert.equal(existsSync(join(root, ".cursorignore")), true); + assert.equal(existsSync(join(root, ".aiexclude")), true); +}); test("textReport with top: 0 omits Top offenders header", () => { const scan = scanRepo(repo); const out = textReport(scan, { price: 3, top: 0 }); @@ -217,4 +234,4 @@ test("textReport with top: 0 omits Top offenders header", () => { !out.includes("Top offenders"), "should not show Top offenders header when top is 0", ); -}); \ No newline at end of file +});