Repository navigation
[Feature]: filee engines: list, install and remove optional engines from the CLI #825
Description
What would you like Filee to do?
The third CLI item from #582, after #714 and #759: commands and JSON shape to agree on before the PR.
filee engines [--json]
filee engines install <id> [--json] [--quiet]
filee engines remove <id> [--json] [--quiet]
<id> is a package id from EngineDownloads.Packages, the ids convert --json already prints in needsEngines.
filee engines (Apple Silicon Mac):
ID STATUS VERSION SIZE
libreoffice not installed 26.2.6 297 MB download, 1.3 GB on disk
pandoc installed 3.13 236 MB on disk
ffmpeg system copy /opt/homebrew/bin
ghostscript not installed Install Ghostscript with Homebrew (brew install ghostscript), then check again or select its bin folder.
{
"platform": "osx-arm64",
"engines": [
{ "id": "pandoc", "status": "installed", "outdated": false, "version": "3.13",
"downloadable": true, "downloadSize": 43148194, "installedSize": 236000000, "converters": ["pandoc"] },
{ "id": "ffmpeg", "status": "owncopy", "fromSystem": true, "folder": "/opt/homebrew/bin",
"downloadable": false, "unavailableReason": "Install FFmpeg with Homebrew (brew install ffmpeg), then check again or select its bin folder.",
"converters": ["ffmpeg"] }
]
}status is notinstalled, installed or owncopy: the resting values of EnginePackageStatus, lowercased like state in convert --json, and worked out the same way as the Engines page. fromSystem is EnginePackageState.FromSystem. Fields that don't apply are left out, not written as null.
filee engines install <id> runs EngineInstaller.InstallAsync (SHA-256 against engines.json, staging folder, marker). If the package is outdated, it installs the new version.
$ filee engines install pandoc
Downloading pandoc 3.13: 18.4 MB / 43.1 MB (43%)
Installed pandoc 3.13.
Progress goes to stderr and the result to stdout. With --json, stdout gets one document at the end:
{ "id": "pandoc", "state": "installed", "version": "3.13" }state is installed, skipped (already up to date), failed or cancelled. failed and cancelled also carry error (and errorKey when there is one), as in convert --json. Ctrl+C cancels through the existing token. The installer then drops the staging folder, so no half-installed engine is left behind.
filee engines remove <id> runs EngineInstaller.Uninstall, only where the Engines page allows it (CanRemove). An own or system copy is never touched: the command prints skipped and says why. Files in use make it failed.
Exit codes stay as they are: 0 nothing failed (skipped included), 1 failed or cancelled, 2 unknown id or wrong arguments.
Platforms: the list includes packages Filee can't download on the running system (on macOS: FFmpeg, Ghostscript, Tesseract, and calibre before macOS 14), with "downloadable": false and EngineDownloads.UnavailableReason. install on one of them prints that reason and exits with 1, with no network access. This is what InstallAsync already does when it throws PlatformNotSupportedException.
Why / when would you use it?
Front ends that embed the CLI (Molt) and scripts already learn from needsEngines and withEngines what is missing, but the only way to install it today is Settings → Engines in the app.
Ideas for how it could work
EngineInstaller, EngineDownloads and EngineEnvironment are in Filee.Engines, which the CLI already references. Two pieces the CLI needs are in Filee.App/Services/EngineDownloadService.cs. I'd move them into Filee.Engines so the app and the CLI share them:
- The
HttpClientwith automatic redirects off and theFilee/<version>user agent, whichEngineInstallerrequires.EngineDownloadServiceandOcrLanguageseach build one today. One factory (for exampleEngineInstaller.CreateHttpClient()) would serve all three. - The status rules from the private
StatusOf: own copy, then Filee's download, then a system copy, plusoutdated. The app keeps the UI-only parts (the version check behindUpdateNote,EnginePackageState).
The PR would also add filee help engines and change the formats <id> hint ("install them in Filee, Settings → Engines") to point at filee engines install <id>. Tests would use EngineInstaller's root and components parameters, without the network.
Not included: OCR language packs, choosing your own copy, LibreOffice options.
Open questions
- Is moving those two pieces into
Filee.EnginesOK? - The app's install queue is in-process. If the app and the CLI install the same package at the same time, both write
.downloads/<id>-<version>.part. ShouldEngineInstallertake a per-package lock file, or is this rare enough to leave? - After installing LibreOffice, should the CLI wait for
WarmUpAsyncas the app does in the background, or leave it to the first conversion? - With
--json, should progress go to stderr as one JSON object per line (the installer'sEngineInstallProgress), or should there be no machine-readable progress?
Unless you prefer otherwise: one id per call, and already-done counts as skipped with exit code 0, as convert does.
Activity
KnifeLemon commentedon Oct 30, 2026
Thanks, @yavuzmeteafsar! This is a clean proposal, and it's what we asked for in #582. The commands, the JSON shape, the exit codes and "one id per call, already done =
skipped" all look right. Go ahead. Answers to your questions:1. Moving the two pieces into
Filee.EnginesYes. Move the
HttpClientfactory (no automatic redirects,Filee/<version>user agent) and the status rules, so the app,OcrLanguagesand the CLI share one copy. The UI-only parts (EnginePackageState, the update note) stay in the app. The Engines page must behave exactly as before; the existing App tests should show that.2. App and CLI installing the same package at once
Take a per-package lock in
EngineInstaller. It's rare, but two writers on one.partfile and oneReplacecould leave a broken engine, which is worse than a clear message..downloads/<id>.lock, opened withFileShare.None), so it works across processes.failedwith an error like "pandoc is being installed by another Filee", and exit code 1.3. LibreOffice warm-up
Don't wait for it. Leave it to the first conversion. A script that installs and then quits shouldn't hang on a warm-up it may never need.
4. Progress with
--jsonJSON lines on stderr, one
EngineInstallProgressobject per line. A front end like Molt can then show a progress bar, and stdout keeps exactly one document at the end. Without--json, the human-readable line on stderr as in your example. With--quiet, nothing on stderr.Smaller points
convert --json(installed,notinstalled,owncopyis fine).FILEE_DATA_DIR, so a test copy never touches the installed Filee's engines.removeon a package with no files of Filee's own (own or system copy):skippedwith the reason, exit code 0, as you wrote.EngineInstaller'srootandcomponentsparameters with a local file, so there's no network:skipped;failed;remove, andremoveof an own copy;I'll assign this to you. Open the PR with
Closes #825. Thanks for the Molt work that keeps feeding these CLI improvements!refactor(engines): share the download client and the status rules
feat(cli): add 'engines' to list, install and remove optional engines