Skip to content

[Feature]: filee engines: list, install and remove optional engines from the CLI #825

Closed

Description

@yavuzmeteafsar

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:

  1. The HttpClient with automatic redirects off and the Filee/<version> user agent, which EngineInstaller requires. EngineDownloadService and OcrLanguages each build one today. One factory (for example EngineInstaller.CreateHttpClient()) would serve all three.
  2. The status rules from the private StatusOf: own copy, then Filee's download, then a system copy, plus outdated. The app keeps the UI-only parts (the version check behind UpdateNote, 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

  1. Is moving those two pieces into Filee.Engines OK?
  2. 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. Should EngineInstaller take a per-package lock file, or is this rare enough to leave?
  3. After installing LibreOffice, should the CLI wait for WarmUpAsync as the app does in the background, or leave it to the first conversion?
  4. With --json, should progress go to stderr as one JSON object per line (the installer's EngineInstallProgress), 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

  1. KnifeLemon commented on Oct 30, 2026

    @KnifeLemon
    Owner

    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.Engines

    Yes. Move the HttpClient factory (no automatic redirects, Filee/<version> user agent) and the status rules, so the app, OcrLanguages and 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 .part file and one Replace could leave a broken engine, which is worse than a clear message.

    • Use a lock file next to the download (e.g. .downloads/<id>.lock, opened with FileShare.None), so it works across processes.
    • The CLI doesn't wait: if the lock is taken, failed with an error like "pandoc is being installed by another Filee", and exit code 1.
    • The app uses the same lock, so an install started from the CLI shows up as a failure there too, not as a second download.

    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 --json

    JSON lines on stderr, one EngineInstallProgress object 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

    • Messages in English (i18n(cli): add European Portuguese and preserve JSON output #552). Values lowercase like convert --json (installed, notinstalled, owncopy is fine).
    • Respect FILEE_DATA_DIR, so a test copy never touches the installed Filee's engines.
    • remove on a package with no files of Filee's own (own or system copy): skipped with the reason, exit code 0, as you wrote.
    • Tests: use EngineInstaller's root and components parameters with a local file, so there's no network:
      • list;
      • install;
      • already installed → skipped;
      • lock held → failed;
      • remove, and remove of an own copy;
      • a package not downloadable on this platform → exit 1 with no network.

    I'll assign this to you. Open the PR with Closes #825. Thanks for the Molt work that keeps feeding these CLI improvements!

  2. added a commit that references this issue on Nov 1, 2026
    13225d8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions