Skip to content

(testing around) Add gfcli remove command to uninstall installed Google Fonts#3

Closed
tinsever wants to merge 2 commits intomasterfrom
cursor/google-fonts-removal-9b14
Closed

(testing around) Add gfcli remove command to uninstall installed Google Fonts#3
tinsever wants to merge 2 commits intomasterfrom
cursor/google-fonts-removal-9b14

Conversation

@tinsever
Copy link
Copy Markdown
Member

@tinsever tinsever commented Mar 22, 2026

Summary

Adds a remove CLI command that deletes font files previously installed with gfcli install, using the same per-user font directories and PascalCase basenames as installation.

Implementation

  • systemFont.uninstall(fileBase) — Tries both .ttf and .woff2 under the same paths used by install (Linux ~/.local/share/fonts, macOS ~/Library/Fonts, Windows %LOCALAPPDATA%\Microsoft\Windows\Fonts). Runs fc-cache -f on Linux when something was removed.
  • googleFont.uninstallAsync(variants) — Resolves variants via the GWFH file map (same as install), then removes matching files.

Usage

gfcli remove "Open Sans"
gfcli remove Roboto --variants regular,700
gfcli remove "Lato","Merriweather"

Tests

Extended google-font and system-font unit tests; npm test and npm run typecheck pass.

Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a new remove command for uninstalling Google Fonts from your system. Specify one or more font families to remove, with support for targeting specific variants. Works across Windows, macOS, and Linux platforms.
  • Tests

    • Added comprehensive test coverage for the font removal feature.

cursoragent and others added 2 commits March 22, 2026 15:56
systemFont.uninstall removes TTF/WOFF2 files from the same per-user
directories used by install. googleFont.uninstallAsync resolves variants
via the GWFH file map and deletes matching basenames. Extend tests and
types accordingly.

Co-authored-by: Tin Sever <mail@tin-sever.de>
Introduces gfcli remove <family...> with optional --variants, matching
install behavior for multi-family and partial success reporting.

Co-authored-by: Tin Sever <mail@tin-sever.de>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

A new font removal feature is introduced across the CLI and library layers. The remove CLI command orchestrates the uninstallation of Google Font families by delegating to GoogleFont.uninstallAsync(), which in turn calls SystemFont.uninstall() to delete font files from platform-specific system directories. TypeScript type definitions and comprehensive test coverage are added to validate the new functionality.

Changes

Cohort / File(s) Summary
CLI removal command
cli.js
Added remove <family...> command that processes font families, calls GoogleFont.uninstallAsync() for each, handles match errors, aggregates results, and exits with appropriate status codes.
Core library methods
lib/google-font.js, lib/system-font.js, lib/types.d.ts
GoogleFont.uninstallAsync(variants) fetches variant URLs, normalizes identifiers, and delegates to SystemFont.uninstall(). SystemFont.uninstall(fileBase) removes .ttf and .woff2 files from platform-specific font directories and refreshes the font cache on Linux. TypeScript interface updated with new method signature.
Test coverage
test/google-font.test.js, test/system-font.test.js
Added unit tests for GoogleFont.uninstallAsync() mocking system font uninstall, and behavioral tests for SystemFont.uninstall() validating file deletion success and ENOENT error handling. Mock extended with uninstall function.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as CLI (cli.js)
    participant GF as GoogleFont<br/>(lib/google-font.js)
    participant SF as SystemFont<br/>(lib/system-font.js)
    participant FS as File System
    participant Cache as fc-cache<br/>(Linux only)

    User->>CLI: remove <family...>
    CLI->>GF: loadFonts()
    CLI->>GF: getFontByNameAsync(family)
    GF-->>CLI: GoogleFont instance
    CLI->>GF: uninstallAsync(variants)
    GF->>GF: Fetch variant→URL map
    GF->>GF: Normalize variant identifiers
    loop For each variant
        GF->>SF: uninstall(PascalCase-fileBase)
        SF->>SF: _uninstallCandidatePaths(fileBase)
        SF->>SF: Compute platform-specific paths
        SF->>FS: Delete .ttf and .woff2 files
        FS-->>SF: File removal result
        alt Linux platform
            SF->>Cache: fc-cache -f
            Cache-->>SF: Cache refresh (best effort)
        end
        SF-->>GF: FontResult[]
    end
    GF-->>CLI: Aggregated FontResult[]
    CLI->>User: Report results & exit status
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop, hop, fonts take flight!
Removal command, speedy and right,
System folders now clean and neat,
A bouncy new feature—oh how sweet!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and accurately describes the primary change: adding a new CLI command gfcli remove for uninstalling Google Fonts. It is concise, specific, and directly matches the changeset's main objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/google-fonts-removal-9b14

Comment @coderabbitai help to get the list of available commands and usage tips.

@tinsever tinsever marked this pull request as ready for review March 22, 2026 21:06
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lib/google-font.js (1)

190-218: LGTM with optional cleanup.

The implementation correctly mirrors installAsync and properly delegates to systemFont.uninstall. The variant resolution via the GWFH file map ensures consistency with installed file basenames.

One optional cleanup: the catch (err) { throw err; } block (lines 211-213) is redundant—the error would propagate naturally without it. The same pattern exists in installAsync, so this is consistent but could be simplified in both places.

♻️ Optional: Remove redundant catch block
 		if (url) {
-			try {
-				const paths = await systemFont.uninstall(fileName + '-' + norm);
-				for (const p of paths) {
-					resultList.push({ family: this.getFamily(), variant: norm, path: p });
-				}
-			} catch (err) {
-				throw err;
+			const paths = await systemFont.uninstall(fileName + '-' + norm);
+			for (const p of paths) {
+				resultList.push({ family: this.getFamily(), variant: norm, path: p });
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/google-font.js` around lines 190 - 218, Remove the redundant try/catch
that simply rethrows the error in googleFont.prototype.uninstallAsync: inside
the loop where you call systemFont.uninstall (and similarly in installAsync if
you want consistency), delete the catch (err) { throw err; } block so the
original error will propagate naturally; locate the try around
systemFont.uninstall call in uninstallAsync and remove the catch only, leaving
the await call and result handling intact.
test/system-font.test.js (1)

138-156: Good test coverage for uninstall success and failure paths.

The tests verify:

  • Successful removal returns paths and calls unlink
  • ENOENT on all files throws "Font file not found"

Minor note: jest.spyOn(fsPromises, 'unlink') on lines 139 and 153 is redundant since it's already set up in beforeEach (line 34). You can simply call .mockResolvedValue() or .mockRejectedValue() on the existing spy.

♻️ Optional: Remove redundant spy setup
 	it('uninstall removes existing font files and returns their paths', async () => {
-		jest.spyOn(fsPromises, 'unlink').mockResolvedValue(undefined);
+		fsPromises.unlink.mockResolvedValue(undefined);
 		const execSpy = jest.spyOn(require('child_process'), 'exec').mockImplementation((cmd, cb) => {
 	it('uninstall throws when no matching files exist', async () => {
-		jest.spyOn(fsPromises, 'unlink').mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
+		fsPromises.unlink.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/system-font.test.js` around lines 138 - 156, The tests for uninstall
currently call jest.spyOn(fsPromises, 'unlink') again even though a spy on
fsPromises.unlink is already created in beforeEach; update the two test cases to
remove the redundant jest.spyOn calls and instead call .mockResolvedValue(...)
or .mockRejectedValue(...) directly on the existing fsPromises.unlink spy,
leaving the exec/child_process spy logic and assertions for systemFont.uninstall
intact so the behavior and expectations for uninstall remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/system-font.js`:
- Around line 260-266: The uninstall search currently only adds the per-user
fonts folder (%LOCALAPPDATA%\Microsoft\Windows\Fonts) for Windows, but install
copies to the system fonts folder (C:\Windows\Fonts) in install; update the
_uninstallCandidatePaths logic so the Windows branch also includes the system
fonts folder used by install (e.g., path.join(process.env.WINDIR ||
process.env.SystemROOT || 'C:\\Windows', 'Fonts')) so that
_uninstallCandidatePaths (and related uninstall routines) can find files
installed by install.

---

Nitpick comments:
In `@lib/google-font.js`:
- Around line 190-218: Remove the redundant try/catch that simply rethrows the
error in googleFont.prototype.uninstallAsync: inside the loop where you call
systemFont.uninstall (and similarly in installAsync if you want consistency),
delete the catch (err) { throw err; } block so the original error will propagate
naturally; locate the try around systemFont.uninstall call in uninstallAsync and
remove the catch only, leaving the await call and result handling intact.

In `@test/system-font.test.js`:
- Around line 138-156: The tests for uninstall currently call
jest.spyOn(fsPromises, 'unlink') again even though a spy on fsPromises.unlink is
already created in beforeEach; update the two test cases to remove the redundant
jest.spyOn calls and instead call .mockResolvedValue(...) or
.mockRejectedValue(...) directly on the existing fsPromises.unlink spy, leaving
the exec/child_process spy logic and assertions for systemFont.uninstall intact
so the behavior and expectations for uninstall remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5ce7250-4504-45e7-82c8-038ece9461ad

📥 Commits

Reviewing files that changed from the base of the PR and between e7512ee and 23e58a8.

📒 Files selected for processing (6)
  • cli.js
  • lib/google-font.js
  • lib/system-font.js
  • lib/types.d.ts
  • test/google-font.test.js
  • test/system-font.test.js

Comment on lines +260 to +266
case 'win32': {
const localAppData = process.env.LOCALAPPDATA;
if (localAppData) {
dirs.push(path.join(localAppData, 'Microsoft', 'Windows', 'Fonts'));
}
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the Windows install path behavior by checking the PowerShell namespace constant

# Search for how install handles Windows to understand the target directory
rg -n -A 10 "Namespace\(0x14\)" --type js

# Check if there are any comments or documentation about Windows font paths
rg -n -i "windows.*font" --type js --type md

Repository: t2sever/gfcli

Length of output: 1599


🌐 Web query:

What is the Shell.Application Namespace(0x14) fonts folder path in Windows 10 and 11?

💡 Result:

In Windows 10 and Windows 11, Shell.Application.Namespace(0x14) (aka ssfFONTS / CSIDL_FONTS) resolves to the system Fonts folder:

Citations:


Windows install/uninstall path mismatch prevents font removal.

The install method (lines 199-236) uses PowerShell with Shell.Application.Namespace(0x14) to copy fonts to the system fonts folder (C:\Windows\Fonts). However, _uninstallCandidatePaths searches for fonts in %LOCALAPPDATA%\Microsoft\Windows\Fonts (the per-user fonts folder).

Fonts installed via this tool will not be found during uninstall on Windows, causing "Font file not found" errors. Update the uninstall path to match the system folder where install places fonts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/system-font.js` around lines 260 - 266, The uninstall search currently
only adds the per-user fonts folder (%LOCALAPPDATA%\Microsoft\Windows\Fonts) for
Windows, but install copies to the system fonts folder (C:\Windows\Fonts) in
install; update the _uninstallCandidatePaths logic so the Windows branch also
includes the system fonts folder used by install (e.g.,
path.join(process.env.WINDIR || process.env.SystemROOT || 'C:\\Windows',
'Fonts')) so that _uninstallCandidatePaths (and related uninstall routines) can
find files installed by install.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

}

return resultList;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uninstall aborts on first missing variant, losing deletions

High Severity

uninstallAsync re-throws immediately when systemFont.uninstall fails for any variant (e.g., a variant that was never installed). When no --variants flag is given, the method attempts all variants from the API. If only a subset was installed, the first non-installed variant throws "Font file not found," aborting the loop. Files already deleted by earlier iterations are gone, but the partial resultList is never returned. The CLI counts this as a failure, leaving the user unaware that some fonts were actually removed. Unlike saveAtAsync, which collects errors and continues, this method uses a catch (err) { throw err; } pattern that makes partial uninstalls both destructive and unreported.

Fix in Cursor Fix in Web

if (localAppData) {
dirs.push(path.join(localAppData, 'Microsoft', 'Windows', 'Fonts'));
}
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows uninstall path mismatches install destination

Medium Severity

On Windows, install uses Shell.Application.Namespace(0x14) which dynamically resolves to either C:\Windows\Fonts (admin) or %LOCALAPPDATA%\...\Fonts (non-admin on Win10 1809+). The new uninstall hardcodes only the %LOCALAPPDATA% per-user path. Fonts installed with admin privileges live in a different directory and will never be found by uninstall, yielding a misleading "Font file not found" error. Additionally, fs.unlink alone doesn't remove the Windows Registry entry (HKCU\...\Fonts), so even when the file is found, the font remains registered and appears broken to applications.

Additional Locations (1)
Fix in Cursor Fix in Web

}
}

return resultList;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove silently succeeds when no files are deleted

Medium Severity

In uninstallAsync, the if (url) guard silently skips any variant whose name doesn't match a key in the GWFH API file map. If all user-specified variants are skipped (e.g., typos or API changes since installation), the function returns an empty array without error. In cli.js, this increments successCount and produces zero output — the user gets no feedback and nothing was removed, yet the command appears to succeed.

Additional Locations (1)
Fix in Cursor Fix in Web

@tinsever
Copy link
Copy Markdown
Member Author

Interesting.

@tinsever tinsever changed the title Add gfcli remove command to uninstall installed Google Fonts (testing around) Add gfcli remove command to uninstall installed Google Fonts Mar 24, 2026
@tinsever tinsever closed this Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants