cTuning
Sign in

cMeta open-source categories, artifacts and automations

One category from cTuningLabs@cmeta-aops.

← All categories

host

Authors: Grigori Fursin. See LICENSE in the project root for details.

License
Apache-2.0
UID
e4e49c251c814f6d
Author
Grigori Fursin
Tags
fleet

Its own commands

CommandWhat it does
backup
v1.py:1594-1747
Copy what only this host has - its configuration, state, secrets and scripts, as the "backup:" block of its
signature

    def backup_(
        self,
        ctx,
        arg1=None,          # host alias
        to=None,            # the folder the dated backup goes into (default: the machine artifact's backup/<host>/)
        dry_run=False,      # list what would be copied, with sizes; copy nothing
        skip_commands=False,  # do not run the host's "backup: commands: before"
        unparsed=None,      # everything after "--" goes to rclone
        project=None,       # named environment; omitted: the host's global environment
    ):

        """
        Copy what only this host has - its configuration, state, secrets and scripts, as the "backup:" block of its
        _desc.yaml declares - into a dated folder of the machine artifact it belongs to:
        <machine>/backup/<host>/<YYYYMMDD-HHMM>/, with backup.yaml saying what came and from where. The machine
        artifact is the asset's record, so the files a rebuild needs sit next to it; "cx host restore" puts them
        back on this host or on its replacement. Root-only files ("sudo_paths") are read through sudo into a
        staging folder on the host first (passwordless sudo), then fetched and removed there.

            cx host backup my-azure-vm-ubuntu --dry_run      # the paths, their kind and size; nothing copied
            cx host backup my-azure-vm-ubuntu
            cx host backup my-azure-vm-ubuntu --to=D:\\backups\\vm
        """
connect
v1.py:494-528
Connect an Android fleet host over Wi-Fi, optionally pair or keep reconnecting.
signature

    def connect_(self, ctx, arg1=None, endpoint=None, serial=None, pair: bool = False,
                 pairing_endpoint=None, keepalive: bool = False, period=10, duration=0,
                 timeout=15, instructions: bool = False):

        """Connect an Android fleet host over Wi-Fi, optionally pair or keep reconnecting.

        cx host connect <watch> --pair       # adb asks for the watch PIN in this terminal
        cx host connect <watch> --endpoint=IP:port
        cx host connect <watch> --keepalive  # foreground loop; stop with Ctrl+C

        Runs on this host's declared local ADB controller. Pairing credentials stay with
        adb; observed device identity and connection endpoint live in the host state.
        """
destroy
v1.py:2245-2312
Destroy an instance cMeta made (cx task run get-host--docker / get-host--wsl): its container is stopped and
signature

    def destroy_(
        self,
        ctx,
        arg1=None,          # the instance (a docker or WSL host made by cx task run get-host--docker / get-host--wsl)
        image=False,        # also remove its image (left alone when another container uses it)
        keep_artifact=False,  # remove the container or distribution, keep the host artifact
        force=False,        # a docker or wsl host declared by hand: remove its container or distribution anyway
    ):

        """
        Destroy an instance cMeta made (cx task run get-host--docker / get-host--wsl): its container is stopped and
        removed through the engine of its parent (a WSL distribution unregistered, its disk with it), the host
        artifact deleted, and with --image its image removed. A docker or wsl host declared by hand needs --force;
        other types are refused. ("rm" and "remove" are the engine's names for "delete", which removes an artifact
        alone - hence "destroy" for the whole instance.)

            cx host destroy my-pc-docker-ubuntu-24.04 [--image]
            cx host destroy my-pc-wsl-ubuntu-26.04
        """
down (stop)
v1.py:1167-1182
Stop a host that can be stopped from here: a WSL distro (wsl -t), a container (docker stop), or
signature

    def down_(
        self,
        ctx,
        arg1=None,          # host alias, UID or wildcard
        tags=None,          # or a selection
        machine=None,
        type=None,
    ):

        """
        Stop a host that can be stopped from here: a WSL distro (wsl -t), a container (docker stop), or
        anything with a declared "commands: down".

            cx host down my-pc-wsl2     (also: cx host stop)
        """
key
v1.py:728-771
The ssh key cx uses to reach a host, as THIS machine finds it, and the public key beside it: the host's
signature

    def key_(
        self,
        ctx,
        arg1=None,          # host alias, or a reference <category>::<artifact>/<file>
    ):

        """
        The ssh key cx uses to reach a host, as THIS machine finds it, and the public key beside it: the host's
        "access: key" - a reference <category>::<artifact>/<file> into a private repository, found through the
        artifact when it is here, else through the copy a secret holds (cx secret which), else ~/.ssh - or a path.
        A reference in place of the host is resolved the same way. Prints paths, never a key. It is what
        "cx task run run-openssh --host=<host>" allows: a machine serves the fleet with the key the fleet reaches it with.

            cx host key lab-laptop-ubuntu
            cx host key fgg.crypt::ssh-x/2026/id_ed25519
        """
list (ls)
v1.py:303-363
The fleet as a table: host, type, machine, (status), OS, sudo, the Docker and Podman clients
signature

    def list_(
        self,
        ctx,
        arg1=None,          # host alias, UID or wildcard (default: all)
        tags=None,          # only hosts with these tags (comma-separated, "-tag" excludes)
        machine=None,       # only the hosts of this machine (alias)
        type=None,          # only this type: ssh, wsl, docker, adb
        status=False,       # check liveness (a TCP probe per ssh host, wsl -l, docker inspect, adb devices)
        active=False,       # like --status, and keep only the hosts that are up (or here)
        details=False,      # with --status: what each parent of a moving device answered, under the row
        d=False,            # same as --details
        sort=None,          # accepted for compatibility with the base command
        skip_uids=False,    # accepted for compatibility with the base command
        project=None,      # one named project; omitted: global environment with project sub-rows
        made=False,         # only the instances cMeta made (get-host--docker: made_from in their _desc.yaml)
    ):

        """
        The fleet as a table: host, type, machine, (status), OS, sudo, the Docker and Podman clients
        ("-" = not installed, "(daemon down)" = a Docker client without a running daemon), the cMeta
        engine version, the cMeta AOPS version (the repository ctuninglabs@cmeta-aops; "none" when the
        host has no AOPS) and the date of the last probe.

            cx host ls
            cx host ls --tags=ubuntu
            cx host ls --machine=my-pc
            cx host ls --active                      # only what answers right now
            cx host ls --made --status               # the instances cMeta made (docker, podman), up or stopped

        OS, cMeta and AOPS come from state.yaml, written by "cx host probe" - "-" means never probed.
        """
probe
v1.py:425-491
Ask every reachable host what it is (OS, kernel, GPU, Python, uv, cx, cMeta version and home,
signature

    def probe_(
        self,
        ctx,
        arg1=None,          # host alias, UID or wildcard (default: all)
        tags=None,          # only hosts with these tags
        machine=None,       # only the hosts of this machine
        type=None,          # only this type
        timeout=DEFAULT_PROBE_TIMEOUT,    # seconds per host
        project=None,      # probe this environment; omitted: global and all declared projects
    ):

        """
        Ask every reachable host what it is (OS, kernel, GPU, Python, uv, cx, cMeta version and home,
        disk, every repository under <cMeta home>/repos with its git branch/commit and _cmr version)
        and write the answer to the host's state.yaml. Declared facts stay in _desc.yaml; state.yaml
        is observed only.

            cx host probe                    # the whole fleet, hosts that are down are skipped and reported
            cx host probe lab-mac-mini-macos # one host: being down is an error
        """
pull
v1.py:1541-1591
Copy a file or a folder of a host here, one way, host -> local: the other way of push, for the results
signature

    def pull_(
        self,
        ctx,
        arg1=None,          # host alias
        arg2=None,          # a file or folder on the host (relative to its login home, or absolute)
        to=None,            # the local folder it goes into (default: the current directory; made when missing)
        files=None,         # only these files of a folder (comma-separated patterns, "*.jsonl,steps.txt")
        dry_run=False,      # show what would be copied, copy nothing
        unparsed=None,      # everything after "--" goes to rclone
        project=None,       # named environment; omitted: the host's global environment
    ):

        """
        Copy a file or a folder of a host here, one way, host -> local: the other way of push, for the results
        of a run there (verdicts, logs, records). A folder's files go into --to with their sub-folders, a file
        goes into --to; nothing here is deleted. rclone is set up when missing (-q: without asking).

            cx host pull lab-laptop-ubuntu cmeta-tests/20261014-1407-versions/work --to=D:\\runs\\t470p -q
            cx host pull lab-mac-mini-macos cmeta-tests/20261014-1407-versions/work --files=*.jsonl --to=D:\\runs\\mac
        """
push
v1.py:1384-1538
Copy a few directories of a repository (or any local path) to a host, one way, local -> host,
signature

    def push_(
        self,
        ctx,
        arg1=None,          # host alias
        arg2=None,          # a repository name from the host's sync list, a repository registered here (its alias), or a local path (then --to)
        dirs=None,          # only these directories/files of it (comma-separated, relative, "task/run-ai/")
        artifacts=None,     # only these artifacts: cRef patterns separated by ";" ("work::2026*;fgg.project::cMeta/!AI"); a partial repository's own by default
        to=None,            # remote path (required for a local path; default for a repository: its remote)
        dry_run=False,      # show what would be copied, copy nothing
        delete=False,       # rclone sync instead of copy: also delete remote files missing locally (within --dirs)
        index=True,         # after a repository push: "cx repo reindex <repository>" on the host (plugged there the first time), so a new artifact is found there (--index- skips it)
        secrets=False,      # a repository of secrets (secrets: true in its _cmr.yaml) goes only with this flag and --artifacts naming the secrets
        unparsed=None,      # everything after "--" goes to rclone
        project=None,      # named environment; omitted: the host's global environment
    ):

        """
        Copy a few directories of a repository (or any local path) to a host, one way, local -> host,
        without touching the bisync baseline - for a quick fix when a sync glitched, or when only one task
        changed. Never deletes unless --delete. After a repository push that repository is rescanned on the host
        ("cx repo reindex <repository>", plugged there when the host does not know it yet; the whole index with a
        cMeta older than 0.34.0), so an artifact new to the host is found by "cx <cat> find" at once; --index- skips
        that (a push of category code or files only).

            cx host push lab-thinkpad-ubuntu ctuninglabs@cmeta-aops --dirs=task/run-ai/,tool/agy/
            cx host push lab-laptop-ubuntu ctuninglabs@cmeta-aops --dirs=task/run-ai/ --dry_run
            cx host push lab-mac-mini-macos D:\\tmp\\check.sh --to=/Users/user/cmeta-tests/check.sh
            cx host push lab-laptop-ubuntu my-repo --artifacts="work::2026*;my.project::x/!AI"
            cx host push lab-laptop-ubuntu my-secrets --artifacts="secret::git-work-without-pass" --secrets

        The repository's blacklist (tmp*/, __pycache__/, .venv/, .git/ ...) still applies. A partial repository
        (artifacts: in the host's sync entry) goes as its selection, never whole; --artifacts writes PARTIAL.md there.
        A repository of secrets goes only by name, with --secrets: into a folder only the host's user can read, then
        "cx secret check --fix" there makes every private key owner-only (the transfer does not keep the mode).
        """
restart (reboot)
v1.py:903-1098
Reboot a host - after a y/N question unless --yes or -q - then wait until it is back and run its
signature

    def restart_(
        self,
        ctx,
        arg1=None,          # host alias (one host)
        yes=False,          # do not ask (also implied by -q)
        wait=True,          # wait until the host answers again (--no-wait: return at once)
        timeout=600,        # seconds to wait for it
        skip_after=False,   # do not run the host's "commands: after_restart"
        skip_check=False,   # do not wait for the host's declared projects afterwards
        check_timeout=300,  # seconds to wait for every project's server and public URLs to answer
        dry_run=False,      # print what would run, restart nothing
    ):

        """
        Reboot a host - after a y/N question unless --yes or -q - then wait until it is back and run its
        "commands: after_restart" (servers to bring up). ssh hosts: "sudo -n shutdown -r now" (Windows:
        "shutdown /r /t 5"); a WSL distro: wsl -t + start; a container: docker restart; adb: adb reboot;
        or the host's own "commands: restart". A host with "projects" is checked like "cx host status" does - each
        server on the host, its public URLs from here - just before the restart and after it, until everything
        that answered before answers again or --check_timeout; one that does not makes the restart fail, a project
        that did not answer before (a server started on demand only) is only reported (--skip_check: no check).

            cx host restart lab-laptop-ubuntu
            cx host restart my-azure-vm-ubuntu --yes      # then its web server comes up again
            cx host restart my-pc-wsl2 --dry_run
        """
restore
v1.py:1750-1905
Put a backup back on a host: every path of "cx host backup" to where it came from, root-only ones through
signature

    def restore_(
        self,
        ctx,
        arg1=None,          # host alias (or an instance cMeta made: back to a snapshot, see below)
        source=None,        # the backup folder (default: the newest backup of this host in its machine artifact)
        into=None,          # put everything under this folder on the host instead of its real place (a test, a look)
        paths=None,         # only these paths of the backup (comma-separated, as backup.yaml names them)
        dry_run=False,      # list what would go where; copy nothing
        yes=False,          # a production host (ai: production: true) restores only with --yes
        skip_commands=False,  # do not run the host's "backup: commands: after_restore"
        name=None,          # an instance's snapshot name (cx host restore <instance> --name=<snapshot>)
        force=False,        # an instance declared by hand (docker, wsl)
        project=None,       # named environment; omitted: the host's global environment
    ):

        """
        Put a backup back on a host: every path of "cx host backup" to where it came from, root-only ones through
        sudo, then the host's "backup: commands: after_restore" (e.g. cx --reindex). The newest backup of the host
        is taken from its machine artifact, --source names another (the backup of the host this one replaces). With
        --into=<folder> everything lands under that folder on the host - a look, or a test on a disposable host.

            cx host restore my-azure-vm-ubuntu --dry_run
            cx host restore new-vm-ubuntu --source=<machine>/backup/my-azure-vm-ubuntu/20261023-1130 --yes
            cx host restore lab-laptop-ubuntu --source=<that folder> --into=cmeta-tests/restore-test

        An instance cMeta made (docker, wsl) goes back to a snapshot instead: cx host restore <instance> [--name=<snapshot>].
        """
run (exec)
v1.py:531-653
Run a command on a host (or on a selection of hosts), through its driver: locally when the host
signature

    def run_(
        self,
        ctx,
        arg1=None,          # host alias (or the command, when --tags/--machine/--type/--all select the hosts)
        arg2=None,          # the command to run there (quote it)
        tags=None,          # run on every host with these tags
        machine=None,       # run on every host of this machine
        type=None,          # run on every host of this type
        all=False,          # run on every host
        script=None,        # a local script file sent to the host's shell on stdin instead of a command
        timeout=None,       # seconds
        capture=False,      # return the output instead of streaming it to the terminal
        raw=False,          # do not wrap the command in the host's shell (ssh/docker: as given)
        interactive=False,  # a terminal for the command (ssh -t / docker exec -it): it can ask questions; needs a terminal here
        project=None,      # named environment; omitted: the host's global environment
        unparsed=None,      # the command as the words after "--" (cx host run <host> -- <command>)
        screen=None,        # keep the command in GNU Screen; bare: cx-host, or --screen=<name>
        screen_title=None,  # title of a new screen window (task delegation supplies its project)
    ):

        """
        Run a command on a host (or on a selection of hosts), through its driver: locally when the host
        is this machine, else ssh / wsl.exe / docker exec / adb shell, wrapped in the host's login shell.

            cx host run lab-laptop-ubuntu "cx --version"
            cx host run lab-laptop-ubuntu -- ls -la /tmp        # the words after "--", cMeta parses none of them
            cx host run --tags=ubuntu "uname -a"
            cx host run lab-thinkpad-ubuntu --script=check.sh
            cx host run lab-thinkpad-ubuntu "cx task run test-python" --interactive   # a terminal there: the command can ask

        The command is passed through the local shell, then ssh, then the remote shell: keep it simple,
        and put anything with quotes of its own in a script (--script). After "--" the words are joined
        with spaces; a word that holds a space gets double quotes (one word for bash and PowerShell alike).
        """
screens
v1.py:774-869
The GNU Screen sessions on a host - the persistent terminals of "cx host ssh --screen" and "--on_screen", and
signature

    def screens_(
        self,
        ctx,
        arg1=None,          # host alias (or a selection: --tags / --machine / --type)
        tags=None,          # a selection by tags
        machine=None,       # the hosts of a machine
        type=None,          # ssh | wsl | docker
        project=None,       # a named environment of the host (its cx)
        quit=None,          # end the session of this name there first (screen -S <name> -X quit: its shell and the run in it), then list
    ):

        """
        The GNU Screen sessions on a host - the persistent terminals of "cx host ssh --screen" and "--on_screen", and
        any other screen of the host's user: name, state, start time where Screen gives it, the titles of the windows
        (a run-ai terminal shows its project; Screen 4.2+ answers -Q, an older one lists none). Read only: nothing is
        changed or installed on the host - no Screen there means no sessions. With --quit=<name> the session of that
        name is ended first (what runs in it dies with its shell), then what is left is listed.

            cx host screens lab-thinkpad-ubuntu
            cx host screens --tags=ubuntu
            cx host screens lab-thinkpad-ubuntu --quit=ai1
        """
show
v1.py:2651-2683
Everything known about a host: the declared _desc.yaml, then the observed state.yaml.
signature

    def show_(
        self,
        ctx,
        arg1=None,          # host alias
        project=None,      # named environment; omitted: the host's global environment
    ):

        """
        Everything known about a host: the declared _desc.yaml, then the observed state.yaml.

            cx host show lab-laptop-ubuntu
        """
snapshot
v1.py:2315-2373
Keep the state of an instance: a docker or podman container is committed to an image
signature

    def snapshot_(
        self,
        ctx,
        arg1=None,          # the instance (a docker, podman or WSL host cMeta made)
        name=None,          # the snapshot's name (default: the time, 20261015-2355)
        note=None,          # a word about it, kept in the record
        force=False,        # a docker or wsl host declared by hand
    ):

        """
        Keep the state of an instance: a docker or podman container is committed to an image
        (cmeta/snapshot:<instance>-<name>; the container keeps running), a WSL distribution exported to a tar
        next to its disk (<location>/snapshots/<name>.tar). The host artifact records it under "snapshots"
        (name, time, image or file, note); "cx host restore <instance> [--name=<snapshot>]" brings the instance
        back to it, "cx host show <instance>" lists them.

            cx host snapshot my-pc-docker-ubuntu-26.04
            cx host snapshot my-pc-docker-ubuntu-26.04 --name=before-torch --note="the clean toolchain"
        """
ssh (shell|login)
v1.py:656-718
Open an interactive shell on a host: ssh, wsl.exe -d, docker exec -it, adb shell.
signature

    def ssh_(
        self,
        ctx,
        arg1=None,          # host alias
        x11=False,          # ssh -Y: X11 forwarding (or "access: x11: true" in the host's _desc.yaml); on Windows DISPLAY=localhost:0.0 unless set
        forward=None,       # ssh -L: local ports to forward to the host, e.g. --forward=1455 or --forward=8080:80,5000
        dry_run=False,      # print the command instead of opening the shell
        screen=None,        # persistent GNU Screen shell: bare = cx-host, or --screen=<name>
    ):

        """
        Open an interactive shell on a host: ssh, wsl.exe -d, docker exec -it, adb shell.

            cx host ssh lab-mac-mini-macos               (also: cx host shell / login)
            cx host ssh lab-thinkpad-ubuntu --x11       # a remote GUI program shows up here
            cx host ssh lab-thinkpad-ubuntu --forward=1455   # a login page the host serves on localhost:1455 opens in this browser
            cx host ssh lab-thinkpad-ubuntu --screen         # create/reconnect cx-host; SSH may disconnect
            cx host ssh lab-thinkpad-ubuntu --screen=work    # a separate shell; exit ends its last window
        """
status
v1.py:366-422
Is each host reachable right now? "here" = this machine, "up"/"down" for the rest,
signature

    def status_(
        self,
        ctx,
        arg1=None,          # host alias, UID or wildcard (default: all)
        tags=None,          # only hosts with these tags
        machine=None,       # only the hosts of this machine
        type=None,          # only this type
        timeout=DEFAULT_STATUS_TIMEOUT,          # seconds per TCP probe (an ssh host)
        cmd_timeout=DEFAULT_STATUS_CMD_TIMEOUT,  # seconds per check that runs a command (adb devices, docker inspect, wsl -l), ssh included
        details=False,      # add the time every check took, and what each parent of a moving device answered
        d=False,            # same as --details
        project=None,      # check only this project's server below each selected host
        made=False,         # only the instances cMeta made (get-host--docker: made_from in their _desc.yaml)
    ):

        """
        Is each host reachable right now? "here" = this machine, "up"/"down" for the rest,
        "missing" for a container or distro that does not exist (yet), "via X down" when the parent is off,
        "timeout" when a check did not answer within --cmd_timeout. A device that moves between machines
        is "up" with "via <parent>" in ADDRESS, or "down" when no parent sees it. One line per host;
        -d adds a TIME column and, under a moving device, what each parent answered and how long it took.

            cx host status
            cx host status lab-laptop-ubuntu
            cx host status -d                      # the time every check took, and what each parent of a moving device said
            cx host status --cmd_timeout=120       # a thorough check of hosts that are slow to answer

        All hosts are checked at once, and so are the candidate parents of a moving device; the table
        appears when the slowest check answers or times out. A cold fleet (peers asleep, adb servers not
        started, caches cold) answers in tens of seconds the first time and in a few seconds after that.
        """
sync
v1.py:1185-1381
Bring a host's repositories up to date, the way its _desc.yaml declares: "bisync" (two-way, rclone
signature

    def sync_(
        self,
        ctx,
        arg1=None,          # host alias, UID or wildcard (default: every host that declares a sync)
        tags=None,          # or a selection
        machine=None,
        type=None,
        repos=None,         # only these repositories (comma-separated names from the host's sync list)
        resync=False,       # bisync: rebuild the baseline (first run of a pair, or after "Must run --resync")
        resync_mode='newer',  # with --resync, which copy wins where both sides have a file: newer | path1 (this machine) | path2 (the host) | older | larger | smaller
        unlock=False,       # bisync: drop a stale lock left by a cancelled run
        dry_run=False,      # show what would change, change nothing (rclone --dry-run; git fetch only)
        skip_after=False,   # do not run the host's "sync: after" commands (e.g. cx --reindex)
        backup=False,       # keep what this run overwrites or deletes: .cmeta-sync-backups/<repository>/<run id>/ beside the repos folder of each side
        project=None,      # named environment; omitted: the host's global environment
        skip_check=False,   # do not compare the two sides after the sync (the check is on by default: rclone check, name and size)
        retries=2,          # how many times a transient failure of a repository is tried again
        pause=True,         # on problems, wait for Enter at the end when a person is at the terminal (never with -q; --pause- turns it off)
    ):

        """
        Bring a host's repositories up to date, the way its _desc.yaml declares: "bisync" (two-way, rclone
        over ssh, the user's _8-bisync scripts), "copy" (one-way local -> host, never deletes), "git"
        (git pull --ff-only on the host's own clone), per repository or by the host's default.

            cx host sync                                  # every reachable host that declares a sync
            cx host sync my-azure-vm-ubuntu      # one host (being down is an error)
            cx host sync --tags=ubuntu --dry_run
            cx host sync lab-thinkpad-ubuntu --repos=ctuninglabs@cmeta-aops --resync --resync_mode=path1

        A few directories only, one-way: "cx host push".
        """
sync_endpoint
v1.py:2635-2649
Resolve a host's SSH transport for sync tasks, without connecting or changing the host.
signature

    def sync_endpoint_(self, ctx, arg1=None):

        """Resolve a host's SSH transport for sync tasks, without connecting or changing the host."""
up (start)
v1.py:1148-1164
Start a host that can be started from here: a WSL distro, a container (created from its image on
signature

    def up_(
        self,
        ctx,
        arg1=None,          # host alias, UID or wildcard
        tags=None,          # or a selection
        machine=None,
        type=None,
    ):

        """
        Start a host that can be started from here: a WSL distro, a container (created from its image on
        the first start), or anything with a declared "commands: up" (a cloud VM: az vm start ...).
        An ssh host without one is reported, not started.

            cx host up my-pc-wsl2       (also: cx host start)
        """
upgrade
v1.py:2000-2243
Update the cMeta engine on a host, as its _desc.yaml declares under "cmeta:":
signature

    def upgrade_(
        self,
        ctx,
        arg1=None,          # host alias, UID or wildcard
        tags=None,          # or a selection
        machine=None,
        type=None,
        version=None,       # a cMeta version to install instead of the latest
        sync=False,         # first bring the host's repositories up to date (cx host sync <host>)
        restart=False,      # then reboot the host and run its "commands: after_restart" (cx host restart <host>)
        yes=False,          # do not ask before the restart (also implied by -q)
        timeout=600,        # seconds to wait for a restarted host
        dry_run=False,      # print the commands, run nothing
        engine=True,        # the cMeta engine (--engine- leaves it alone, e.g. with --system only)
        system=False,       # the OS packages too, with "cx task run upgrade-os" on the host (needs passwordless sudo there)
        firmware=False,     # the firmware too (upgrade-os --firmware: fwupd on Linux)
        check=False,        # with --system/--firmware: only list what would be upgraded (upgrade-os --check)
        project=None,      # named environment; omitted: the host's global environment
    ):

        """
        Update the cMeta engine on a host, as its _desc.yaml declares under "cmeta:":
        install "uv-tool" -> uv tool upgrade cmeta (or uv tool install --force "cmeta==<version>");
        install "pip" -> pip install -U cmeta; install "editable" -> git pull in the source tree;
        or the literal "cmeta: upgrade" command. Then prints the new "cx --version".

        --system (and --firmware) also runs the task upgrade-os on the host: the OS packages with the OS's
        own commands (apt, dnf, zypper, pacman, apk, softwareupdate + brew, winget), the firmware with fwupd,
        and reports whether the OS asks for a reboot; --check only lists what would be upgraded.

        The full update of a host that runs servers is sync, upgrade, restart - in that order, since the
        upgrade replaces the engine under the running servers and only the restart brings them up on the
        new one (through "commands: after_restart"): --sync and --restart do the three steps per host.

            cx host upgrade lab-laptop-ubuntu
            cx host upgrade --tags=ubuntu --version=0.33.0
            cx host upgrade my-azure-vm-ubuntu --sync --restart --yes
            cx host upgrade lab-laptop-ubuntu --system --firmware --check     # what the OS would upgrade
            cx host upgrade lab-laptop-ubuntu --engine- --system --restart --yes
        """
whoami
v1.py:3397-3440
The host artifact of the machine this command runs on: the ssh host whose probed system_id is the local
signature

    def whoami_(
        self,
        ctx,
    ):

        """
        The host artifact of the machine this command runs on: the ssh host whose probed system_id is the local
        one (an id that stays across reboots and renames), else the one whose declared hostname is the local one.
        Prints its alias; returns {'host': alias, 'uid', 'machine', 'by': 'system_id' | 'hostname'}, or
        {'host': ''} when none is (an instance names itself after the hostname then).

            cx host whoami
        """

Common commands

Every category also inherits the commands below, so any artifact can be found, read, created, moved and tagged the same way.

CommandWhat it does
copy (cp)Copy artifact(s).
signature

    def copy__(
        self,
        params: dict,  # cMeta params.
    ):

        """
            Copy artifact(s).

            @base.move_(**params, copy=True)

            Args:
                params (dict): cMeta params.

            Returns:
                dict: A cMeta dictionary (see move_ for details).

            Raises:
                Exception: Propagated runtime errors, if any.
        """
create (add)Create and index an artifact.
signature

    def create_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Tags to add to the artifact.
        meta: dict = {},  # Initial metadata dictionary.
        yaml: bool = False,  # Save metadata as YAML instead of JSON.
        virtual: bool = False,  # Virtual artifact created only in index (such as repo).
        path: str = None,  # Use this path for artifact (useful for virtual artifacts such as repo).
        index: bool = False, # If true and path exists but not index, just add to index
                             # Useful when AI agents create cMeta entries while bypassing cMeta
    ):

        """
            Create and index an artifact.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Tags to add to the artifact.
                meta (dict): Initial metadata dictionary.
                yaml (bool): Save metadata as YAML instead of JSON.
                virtual (bool): Virtual artifact created only in index (such as repo).
                path (str | None): Use this path for artifact (useful for virtual artifacts such as repo).
                index (bool): Index an artifact that already exists at `path` without creating it.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **path** (str): Path to the created artifact.
                    - **meta** (dict): Artifact metadata.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
delete (rm|remove|del)Delete and unindex artifact(s).
signature

    def delete_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Optional tag filter.
        sort: bool = True,  # Sort artifacts by alias and UID.
        force: bool = False,  # Force deletion without confirmation.
        f: bool = False,  # Shorthand for force.
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
        ignore_errors: bool = False,  # Ignore errors when deleting multiple artifacts.
        print_time: bool = False,  # Print time per deletion.
        match: dict = None,  # Value for match.
        match_empty_version: bool = False,  # Value for match empty version.
        all_tags: str = None,  # Value for all tags.
    ):

        """
            Delete and unindex artifact(s).

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Optional tag filter.
                sort (bool): Sort artifacts by alias and UID.
                force (bool): Force deletion without confirmation.
                f (bool): Shorthand for force.
                skip_uids (bool): Skip UIDs when using wildcards.
                ignore_errors (bool): Ignore errors when deleting multiple artifacts.
                print_time (bool): Print time per deletion.
                match (dict | None): Match artifact metadata before deleting.
                match_empty_version (bool): Match artifacts with an empty version value.
                all_tags (str | None): Require all supplied tags when selecting artifacts.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): All matched artifacts.
                    - **deleted_artifacts** (list): Deleted artifacts.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
find (search)Find artifacts.
signature

    def find_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Comma-separated string or iterable of tags to match.
        sort: bool = None,  # Sort by path (True by default).
        add_index_file: bool = False,  # Add index file information.
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
        match: dict = None,  # Value for match.
        match_empty_version: bool = False,  # Value for match empty version.
        match_empty_values: bool = False,  # Match when queried values or keys are empty.
        all_tags: str = None,  # Value for all tags.
        load_files = [],  # Load files (omit extension to load either yaml or json)
        follow_migrated: bool = True,  # Follow an old alias left by "migrate" to its artifact.
    ):

        """
            Find artifacts.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Comma-separated string or iterable of tags to match.
                sort (bool): Sort by path (True by default).
                add_index_file (bool): Add index file information.
                skip_uids (bool): Skip UIDs when using wildcards.
                match (dict): Value for match.
                match_empty_version (bool): Value for match empty version.
                match_empty_values (bool): Match artifacts whose queried values or keys are empty.
                all_tags (str): Value for all tags.
                load_files (list): Metadata file basenames to load from each matched artifact.
                follow_migrated (bool): An alias given alone that is a migration stub (left by
                    `migrate`) stands for the artifact it points to, with a notice; False returns
                    the stub itself. A UID always finds exactly what it names.
            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): List of matched artifacts.

            Raises:
                Exception: Propagated runtime errors, if any.                           \
        """
getGet artifact meta. Read if exists, create and read if doesn't exist.
signature

    def get_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Tags to add to the artifact (used during creation).
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
        yaml: bool = False,  # Save/output metadata as YAML instead of JSON.
        meta: dict = {},  # Initial metadata dictionary (used during creation).
        virtual: bool = False,  # Virtual artifact created only in index (used during creation).
        path: str = None,  # Use this path for artifact (used during creation).
        show_path: bool = False,  # Print path instead of meta
        load_files = [],  # Value for load files.
    ):

        """
            Get artifact meta. Read if exists, create and read if doesn't exist.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Tags to add to the artifact (used during creation).
                skip_uids (bool): Skip UIDs when using wildcards.
                yaml (bool): Save/output metadata as YAML instead of JSON.
                meta (dict): Initial metadata dictionary (used during creation).
                virtual (bool): Virtual artifact created only in index (used during creation).
                path (str | None): Use this path for artifact (used during creation).
                show_path (bool): Print path instead of meta
                load_files: Value for load files.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifact** (dict): Matched artifact.
                    - **cmeta** (dict): Artifact metadata.
                    - **created** (bool): True if artifact was created, False if it already existed.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
indexIndex artifact(s).
signature

    def index(
        self,
        params: dict,  # cMeta params.
    ):

        """
            Index artifact(s).

            @base.create_(**params, index=True)

            Args:
                params (dict): cMeta params.

            Returns:
                dict: A cMeta dictionary (see move_ for details).

            Raises:
                Exception: Propagated runtime errors, if any.
        """
infoGet artifact info.
signature

    def info_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        clip: bool = True,  # If True, copy cRef to clipboard.
        url: bool = False,  # If True, copy URL with detected cRef to the clipboard.
        name: bool = False,  # If True, copy artifact name to the clipboard
    ):

        """
            Get artifact info.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                clip (bool): If True, copy cRef to clipboard.
                url (bool): If True, copy URL with detected cRef to the clipboard.
                name (bool): If True, copy artifact name to the clipboard
            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): List of matched artifacts.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
list (ls)List artifact names.
signature

    def list_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Optional tag filter.
        sort: bool = None,  # Sort by alias or UID.
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
    ):

        """
            List artifact names.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Optional tag filter.
                sort (bool): Sort by alias or UID.
                skip_uids (bool): Skip UIDs when using wildcards.
            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): List of matched artifacts.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
migrateRename an artifact (or move it to another repository under a new alias) and leave a
signature

    def migrate_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str,  # The artifact to migrate: alias, UID or alias,UID (one artifact, no wildcards).
        arg2: str,  # Its new alias, optionally in another repository: [repo:]alias.
        tags: str = None,  # Prune the source artifact by tags.
    ):

        """
            Rename an artifact (or move it to another repository under a new alias) and leave a
            stub under its old alias that points to it.

            The artifact keeps its UID, as with `move`, so `alias,UID` and UID references to it keep
            working. The stub gets the old alias, a new UID and two keys: `migrated_to`, the new
            `alias,UID`, and `migrated_when`, the date and time of the migration (ISO 8601, UTC).
            A lookup of the old alias alone - `cx task run <old>`, `cx <category> find <old>` - then
            follows the stub to the artifact and prints a notice once. Commands that change
            artifacts (update, tags, move, delete) act on the stub itself, so `cx <category> rm <old>`
            removes it when nobody uses the old alias any more.

            Args:
                ctx (dict): cMeta context.
                arg1 (str): The artifact to migrate: alias, UID or alias,UID (one artifact, no wildcards).
                arg2 (str): Its new alias, optionally in another repository: [repo:]alias.
                tags (str | list | None): Prune the source artifact by tags.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifact** (dict): The migrated artifact (its new alias, the same UID).
                    - **stub_path** (str): Path of the stub left under the old alias.
                    - **stub_meta** (dict): Meta of the stub.
                    - **migrated_to** (str): The new `alias,UID`.
                    - **migrated_when** (str): When, ISO 8601 in UTC.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
move (ren|rename|mv)Rename and/or move artifact(s).
signature

    def move_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str,  # cMeta artifact(s) with wildcards to be renamed or moved.
        arg2: str,  # New cMeta artifact name and/or new repository.
        tags: str = None,  # Prune source artifacts by tags.
        sort: bool = True,  # Sort artifacts by alias and UID when updating in batch.
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
        ignore_errors: bool = False,  # Ignore errors when updating multiple artifacts.
        copy: bool = False,  # If True, copy artifact instead of moving.
    ):

        """
            Rename and/or move artifact(s).

            Args:
                ctx (dict): cMeta context.
                arg1 (str): cMeta artifact(s) with wildcards to be renamed or moved.
                arg2 (str): New cMeta artifact name and/or new repository.
                tags (str | list | None): Prune source artifacts by tags.
                sort (bool): Sort artifacts by alias and UID when updating in batch.
                skip_uids (bool): Skip UIDs when using wildcards.
                ignore_errors (bool): Ignore errors when updating multiple artifacts.
                copy (bool): If True, copy artifact instead of moving.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): All matched artifacts.
                    - **updated_artifacts** (list): Updated artifacts.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
read (load)Read artifact meta.
signature

    def read_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Optional tag filter.
        extra: bool = False,  # Show extra info.
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
        yaml: bool = False,  # Output as YAML instead of JSON.
        load_files = [],  # Value for load files.
    ):

        """
            Read artifact meta.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Optional tag filter.
                extra (bool): Show extra info.
                skip_uids (bool): Skip UIDs when using wildcards.
                yaml (bool): Output as YAML instead of JSON.
                load_files: Value for load files.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifact** (dict): Matched artifact.
                    - **cmeta** (dict): Artifact metadata.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
reindexRefresh the index record(s) of artifact(s) from their folders, changing no file.
signature

    def reindex_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias, UID, alias,UID or repo:alias (wildcards allowed); nothing names every artifact of the category.
        tags: str = None,  # Prune the artifacts by tags, as in find (a tag with a leading "-" excludes).
        sort: bool = True,  # Sort the artifacts by alias and UID.
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
        ignore_errors: bool = False,  # Go on past an artifact that cannot be reindexed and report it at the end.
    ):

        """
            Refresh the index record(s) of artifact(s) from their folders, changing no file.

            The meta file of each artifact is read from its folder on disk and the artifact's record in the
            index of the category is rewritten from it: the alias is the folder's name, every other alias
            that still pointed at the artifact's UID is dropped, and the path, the meta and the sharding
            depth are what a full `cx --reindex` records. The meta file is read, never written (an empty
            `update` rewrites it). Use it after a meta file was edited by hand, after a folder was renamed,
            moved or copied in from another machine, or when a lookup looks stale - instead of the whole
            `cx --reindex`.

            The artifact may be named by its alias, UID, alias,UID or repo:alias, with wildcards; nothing
            names every artifact of the category. A folder the index does not know yet (made or renamed by
            hand) is found on disk in the repositories; an artifact whose indexed folder is gone is looked
            for by its UID in its repository. No folder is touched, and the only record ever removed is
            that of an artifact whose folder, still there, holds another artifact now (replaced by hand) -
            said in the output. A folder that cannot be found, a meta without a valid `artifact` UID, or a
            UID found in two existing folders is an error, and the index is left as it was
            (`--ignore_errors` reindexes the other artifacts and reports these).

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias, UID, alias,UID or repo:alias (wildcards allowed); nothing
                    names every artifact of the category.
                tags (str | list | None): Prune the artifacts by tags, as in find (a leading "-" excludes).
                sort (bool): Sort the artifacts by alias and UID.
                skip_uids (bool): Skip UIDs when using wildcards.
                ignore_errors (bool): Go on past an artifact that cannot be reindexed and report it at the end.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error (16: nothing found).
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): The reindexed artifacts, as their new index records.
                    - **dropped_aliases** (dict): Per UID, the (lowercase) aliases dropped because they were not the folder's name.
                    - **dropped_records** (list): The records removed because their folder holds another artifact now (`ref`, `path`, `now`).
                    - **errors** (list): With `ignore_errors`, one dict per artifact that could not be reindexed (`ref`, `path`, `error`).

            Raises:
                Exception: Propagated runtime errors, if any.
        """
setSet artifact meta. Update and read if exists, create and read if doesn't exist.
signature

    def set_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Tags to add to the artifact (used during creation).
        skip_uids: bool = False,  # Skip UIDs when using wildcards.
        yaml: bool = False,  # Save/output metadata as YAML instead of JSON.
        meta: dict = {},  # Initial metadata dictionary (used during creation).
        virtual: bool = False,  # Virtual artifact created only in index (used during creation).
        path: str = None,  # Use this path for artifact (used during creation).
        show_path: bool = False,  # Print path instead of meta
    ):

        """
            Set artifact meta. Update and read if exists, create and read if doesn't exist.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Tags to add to the artifact (used during creation).
                skip_uids (bool): Skip UIDs when using wildcards.
                yaml (bool): Save/output metadata as YAML instead of JSON.
                meta (dict): Initial metadata dictionary (used during creation).
                virtual (bool): Virtual artifact created only in index (used during creation).
                path (str | None): Use this path for artifact (used during creation).
                show_path (bool): Print path instead of meta

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifact** (dict): Matched artifact.
                    - **cmeta** (dict): Artifact metadata.
                    - **created** (bool): True if artifact was created, False if it already existed.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
tagsFind unique tags in artifacts.
signature

    def tags_(
        self,
        ctx: dict,  # cMeta context.
        arg1: str = None,  # Artifact alias or UID.
        tags: str = None,  # Comma-separated string or iterable of tags to match.
        sort: bool = False,  # Value for sort.
    ):

        """
            Find unique tags in artifacts.

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): Artifact alias or UID.
                tags (str | list | None): Comma-separated string or iterable of tags to match.
                sort (bool): Value for sort.
            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): List of matched artifacts.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
testTest category.
signature

    def test(
        self,
        params: dict,  # Parameters dictionary.
    ):

        """
            Test category.

            Args:
                params (dict): Parameters dictionary.
            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.

            Raises:
                Exception: Propagated runtime errors, if any.
        """
updateUpdate artifact(s).
signature

    def update_(
        self,
        ctx: dict,  # Execution context dictionary with category, command, and control data.
        arg1: str = None,  # First positional argument from command input.
        tags: str = None,  # Value for tags.
        sort: bool = True,  # Value for sort.
        skip_uids: bool = False,  # If True, skip UID validation in wildcard lookups.
        meta: dict = {},  # Metadata dictionary to persist.
        new_tags: str = None,  # Value for new tags.
        replace_lists: bool = False,  # Value for replace lists.
        replace: bool = False,  # Value for replace.
        ignore_errors: bool = False,  # Value for ignore errors.
        create: bool = False,  # Value for create.
        create_params: dict = {},  # Value for create params.
        update_category: bool = False,  # Value for update category.
        match: dict = None,  # Value for match.
        match_empty_version: bool = False,  # Value for match empty version.
        all_tags: str = None,  # Value for all tags.
    ):

        """
            Update artifact(s).

            Args:
                ctx (dict): cMeta context.
                arg1 (str | None): cMeta artifact(s) with wildcards.
                tags (str | list | None): Prune artifacts by tags.
                sort (bool): Sort artifacts by alias and UID when updating in batch.
                skip_uids (bool): Skip UIDs when using wildcards.
                meta (dict): Meta dictionary to merge recursively with existing artifact.
                new_tags (str | list | None): Add more tags.
                replace_lists (bool): Replace lists during merging.
                replace (bool): Replace existing meta dictionary entirely.
                ignore_errors (bool): Ignore errors when updating multiple artifacts.
                create (bool): If artifact doesn't exist attempt to create.
                create_params (dict): Pass params to create function.
                update_category (bool): Allow category update in meta.
                match (dict | None): Match artifact metadata before updating.
                match_empty_version (bool): Match artifacts with an empty version value.
                all_tags (str | None): Require all supplied tags when selecting artifacts.

            Returns:
                dict: A cMeta dictionary with the following keys:
                    - **return** (int): 0 if success, >0 if error.
                    - **error** (str): Error message if `return > 0`.
                    - **artifacts** (list): All matched artifacts.
                    - **updated_artifacts** (list): Updated artifacts.

            Raises:
                Exception: Propagated runtime errors, if any.
        """

How to use it

cx host backup
cx host                     # list every command with its help
from cmeta import CMeta
cm = CMeta()
r = cm.access({'category': 'host,e4e49c251c814f6d', 'command': 'backup'})

Artifacts

The open-source repositories ship no host artifacts - this category holds what you and your runs create.

cx host list

Depends on

Uses categories: category, machine, result, secret, task, utils

Uses artifacts: task::keep-adb-connection-alive, task::rclone-to-ssh

Source: host on GitHub · its API

Read live from the cMeta index of this server - the same data cx category list shows you locally.