# `Hyper`
[🔗](https://github.com/harmont-dev/hyper/blob/main/lib/hyper.ex#L1)

`Hyper` is a distrubuted elixir virtual machine orchestrator.

# `create_vm`

```elixir
@spec create_vm(Hyper.Vm.Spec.t()) :: {:ok, Hyper.Vm.t()} | {:error, term()}
```

Create a new virtual machine from an image.

Placement: scheduled onto the most available node (`Hyper.Cluster.Scheduler`),
preferring nodes that already have the image's layers resident. On the chosen
node a per-VM writable rootfs is built over the image and the guest is booted.

# `exec`

```elixir
@spec exec(Hyper.Vm.t() | Hyper.Vm.Id.t(), [String.t()], keyword()) ::
  {:ok, %{stdout: binary(), stderr: binary(), exit_code: integer()}}
  | {:error, term()}
```

Run `argv` inside VM `vm` — a pid (from `create_vm/1`) or a `Hyper.Vm.Id.t()`
binary — and return its captured output.

Cluster-routed: a pid resolves to its owning node directly (`node(pid)`); a
vm_id resolves via the routing registry (`whereis/1`). The command runs on
whichever node hosts the VM, via that node's `Hyper.Node.exec/3`.

Returns `{:ok, %{stdout: binary(), stderr: binary(), exit_code: integer()}}`, or
`{:error, reason}` — `:agent_unavailable` if the relay/agent is not yet
reachable, `:timeout` if the gRPC deadline expired, `:not_found` if the VM
cannot be resolved (unknown vm_id, or a pid whose id/owning node is gone),
`:node_unreachable` if the owning node cannot be reached over the cluster.
`opts`: `:env` (map `%{String.t() => String.t()}`), `:cwd` (string), `:timeout`
(ms, gRPC deadline forwarded to the relay; default 30 000).

`argv` is executed directly, not through a shell. Use an **absolute** `argv`
head (`["/bin/echo", "hi"]`): the guest runs as PID 1 with a near-empty
environment, and a non-empty `:env` *replaces* it entirely, so a bare command
name has no `PATH` to resolve against and returns exit code 127.

# `fork_vm`

```elixir
@spec fork_vm(Hyper.Vm.Id.t()) :: {:ok, Hyper.Vm.t()} | {:error, term()}
```

Fork the running VM `vm_id`: boot a child from a copy-on-write clone of its
disk, colocated when the parent's node has room and re-placed cluster-wide
otherwise. Returns the child VM. See `Hyper.Vm.fork/1` for the semantics.

# `id`

```elixir
@spec id(Hyper.Vm.t()) :: Hyper.Vm.Id.t() | nil
```

The vm id for a VM handle -- the pid returned by `create_vm/1`. Resolves on the
pid's owning node, so a VM just placed on a remote node is found immediately
rather than waiting for the routing CRDT to propagate. `nil` if unknown.

Returns `nil` (rather than crashing the caller) if the owning node is
unreachable -- e.g. it died with a VM just placed on it. In that case the VM
died with its host, so "unknown" is the truthful answer. Only `:erpc`'s own
transport failures are swallowed; a genuine fault in the lookup still raises.

# `stop_vm`

```elixir
@spec stop_vm(Hyper.Vm.Id.t()) :: :ok | {:error, :not_found | :machine_unreachable}
```

Stop and tear down `vm_id`, wherever it runs in the cluster.

Resolves the VM's supervisor through the routing registry and terminates it
on its owning node (`Hyper.Node.stop_image_vm/1`), which unwinds the whole
per-VM tree: guest killed, writable volume reclaimed, routing entry removed.
Registry removal propagates via CRDT, so `whereis/1` may briefly still
resolve a just-stopped VM on other nodes.

Returns `{:error, :not_found}` when no such VM is registered anywhere, and
`{:error, :machine_unreachable}` when the owning node cannot be reached.

# `usage`

```elixir
@spec usage(Hyper.Vm.t() | Hyper.Vm.Id.t()) ::
  {:ok, Unit.Time.t()} | {:error, :not_found}
```

Total CPU time `vm` has **actually executed**, metered from its cgroup: the
sum of its flushed usage windows (`Hyper.Metering.Usage`) plus the owning
node's not-yet-flushed remainder. An idle VM accrues (almost) nothing — this
measures compute performed, not compute allocated, and is the billing
counter.

Works for live and stopped VMs alike; a stopped VM reports its recorded
lifetime total. Returns `{:error, :not_found}` only when `vm` is entirely
unknown: not running anywhere and never metered. If the owning node is
unreachable, the flushed total alone is returned — an under-count of at most
one flush window (~60s), consistent with metering's crash guarantee.

# `whereis`

```elixir
@spec whereis(Hyper.Vm.Id.t()) :: node() | nil
```

Cluster-wide: which node currently runs `vm_id`? `nil` if unknown.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
