Skip to content

Repository files navigation

fleetctl

Plugin-based fleet management for the devices around your house —
Fire TV sticks, NVIDIA Shields, PCs — plus the software running on them.

CI License: MIT Python 3.12+ Typed Coverage

Last verified 2026-08-14

fleetctl discovers devices on a subnet, captures and rebuilds Kodi profiles, and deploys them over ADB or SSH. It runs from the command line, over MCP, and through a Home Assistant integration.

Why

fleetctl separates three things that usually get tangled together:

Ring Knows about Examples
core nothing device-specific transport, inventory, discovery, artifacts, operations, workflows, audit
packs what a device is firetv, shield, steamdeck, linux_host
apps software on a device kodi

Dependencies point inward only. An app pack never imports a device pack — its steps declare the Capability values they need (exec, files, state, ...) and the engine resolves which pack on the target actually provides them. That's what lets one Kodi build deploy to a Fire Stick and a Shield without either knowing the other exists.

flowchart TB
    subgraph apps["apps/ — software on a device"]
        direction LR
        KODI["kodi"]
    end

    subgraph packs["packs/ — what a device is"]
        direction LR
        FIRETV["firetv"]
        SHIELD["shield"]
        DECK["steamdeck"]
        LINUX["linux_host"]
    end

    subgraph core["core/ — device-agnostic kernel"]
        direction LR
        T["transport"]
        I["inventory"]
        W["workflow"]
        AR["artifacts"]
        O["observability"]
    end

    KODI -. "declares capabilities —<br/>never imports a pack" .-> packs
    apps ==> core
    packs ==> core

    classDef a fill:#7c3aed,stroke:#5b21b6,color:#fff
    classDef p fill:#ea580c,stroke:#c2410c,color:#fff
    classDef c fill:#0d9488,stroke:#0f766e,color:#fff
    class KODI a
    class FIRETV,SHIELD,DECK,LINUX p
    class T,I,W,AR,O c
Loading

Vendor packs compose a shared base rather than subclassing each other — firetv and shield both build on packs/android, steamdeck and linux_host on packs/posix. That is what keeps Amazon's bugs out of the Shield: a quirk lives as data on the pack that actually has it.

One capture, many targets

Capture reads a profile off a known-good device; build reshapes it into an artifact; deploy sends that artifact to every device that can take it. The split is structural, not stylistic — build gets a transform chain and no transport, deploy gets a transport and no transform chain — so nothing can quietly rewrite a payload on its way to a device.

flowchart LR
    GOLD["gold device"] -->|capture| RAW["profile archive"]
    RAW --> BUILD{"build<br/>transform chain"}
    BUILD --> ART[("artifact store<br/>SMB or local")]
    ART -->|deploy| D1["fire stick"]
    ART -->|deploy| D2["shield"]
    ART -->|deploy| D3["steam deck"]

    classDef g fill:#7c3aed,stroke:#5b21b6,color:#fff
    classDef b fill:#005288,stroke:#003d66,color:#fff
    classDef s fill:#0d9488,stroke:#0f766e,color:#fff
    classDef d fill:#ea580c,stroke:#c2410c,color:#fff
    class GOLD g
    class BUILD b
    class ART s
    class D1,D2,D3 d
Loading

Install

Requires Python 3.12+ and uv.

git clone https://github.com/salvuswarez/fleetctl.git
cd fleetctl
uv sync --all-extras
uv run fleetctl --version

Then see docs/getting-started.md for first-run config and pointing it at a real device.

Development

uv sync --all-extras           # install everything, including dev tools
uv run pytest                  # tests (900+)
uv run pytest --cov=src        # with coverage (gate: 90%)
uv run black src tests         # format
uv run isort src tests         # import order
uv run mypy                    # strict type checking
uv build                       # wheel + sdist

CI runs all of the above on every push and pull request.

Configuration

Config is YAML and lives under config/, which is gitignored — only *.example files are tracked. Config files hold secret references, never secret values:

artifacts:
  smb:
    host: 192.168.1.50
    user: !ref env:SMB_USER
    password: !ref env:SMB_PASS

!ref env:NAME resolves against the process environment at load time and is wrapped in a Secret afterward, so a fleet.yml is safe to paste into a bug report. Full field reference: docs/configuration.md.

Safety

fleetctl runs destructive operations against real hardware: wiping app profiles, disabling system packages, deploying to every tagged device. Three things keep that honest:

  • Effect classification. Every step declares itself read, mutating, or destructive. Policy keys off the class, not a list of names.
  • Protected devices. Devices can be marked off-limits for named steps via config, no code change.
  • Audit trail. Every mutating or destructive command is recorded — actor, target, outcome, duration — in an append-only, hash-chained log.

Because the gate keys off the declared class rather than a name, a new step is governed the day it is written — there is no list to remember to update:

flowchart LR
    S["step"] --> E{"declared<br/>effect class"}
    E -->|read| RUN["runs"]
    E -->|mutating| POL{"policy"}
    E -->|destructive| POL
    POL -->|permitted| RUN
    POL -->|needs approval| APR["waits for<br/>--approve"]
    POL -->|protected device<br/>or over blast radius| NO["refused"]
    APR --> RUN
    RUN --> AUD[("audit log<br/>hash-chained")]

    classDef ok fill:#0d9488,stroke:#0f766e,color:#fff
    classDef gate fill:#005288,stroke:#003d66,color:#fff
    classDef bad fill:#b91c1c,stroke:#7f1d1d,color:#fff
    classDef wait fill:#b45309,stroke:#7c2d12,color:#fff
    class RUN,AUD ok
    class POL,E gate
    class NO bad
    class APR wait
Loading

See docs/safety.md and SECURITY.md.

Licence

MIT — see LICENSE.






fleetctl

Overview

Documentation

Repositories

References

About

  • Plugin-based home device fleet manager
  • MIT licensed


fleetctl

About

device management

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages