WARNING: Hobby project under rapid development. Use at your own risk!
Kai is a CLI that makes using determinate systems easy, friendly, and fun.
Imagine everything about how your computer works is a portable config in one file you can just send to your friends or bring with you to a new computer. Spend very little time thinking about installing software, dependencies, developer environments, etc., and once you figure it out once, you shouldn't have to figure it out again. We have that, with Nix! The problem is Nix is so hard to learn and use that people often give up (even agents get confused!). This is a complete paradigm shift in how we interact with software! But if no one uses it, what's the point?
To attempt a solution, kai wraps nix in a friendly frontend so that you can actually use it with confidence.
kai can already enter shells with the other determinate system, Guix, and plans to support more of it. If we're lucky, maybe it will even get a custom implementation which learns from the mistakes of the others 👀
The goal is to make using determinate systems so easy and powerful that they become the de-facto choice for computer use in all its forms: from desktops to servers and beyond. Practically, this means adopting Nix under the hood and creating useful abstractions on top in the short term, like jujutsu does with git.
A personal motivation is to stimulate not just Linux adoption but determinate computing adoption by eventually creating a custom NixOS-based competitor to Omarchy.
Kai runs on Linux and needs Nix with flakes enabled.
One immediate benefit of a determinate system is you can do things like this!
nix run github:thebrandonlucas/kai -- --version
nix shell github:thebrandonlucas/kai # kai on PATH in a new shellThe Nix package includes the Roc compiler Kai uses to evaluate Kaifile.roc
and pre-seeds Roc's package cache with the matching Kai platform.
Release archives contain only the kai binary. To use one, also install Nix
and the Roc compiler named in .roc-version (available from
roc-overlay), either on PATH or
set as ROC=/path/to/roc. Roc downloads the platform the first time it checks
a Kaifile.roc.
A project is configured by Kaifile.roc, an ordinary
Roc app on Kai's plugin platform. Its kaifile is
made of plugins; Kai's standard plugin, std, takes a list of settings:
app [kaifile] {
pf: platform "https://github.com/thebrandonlucas/kai/releases/download/v0.0.8/8eM3r3RE4n1BFSWqD7wvqch5UW3ACJ7LLJiiin5nxJdK.tar.zst",
std: "https://github.com/thebrandonlucas/kai/releases/download/v0.0.8/std-6zJHFrCtGkKhyuiNbmnJNHPx2DQvcgUeDL2znXw86ZEx.tar.zst",
}
import std.Std
kaifile = Std.kaifile([
Name("hello"),
Systems(["x86_64-linux", "aarch64-linux"]),
Environment("dev", [Tools(["cowsay", "python3"])]),
Shell("default", [Use("dev")]),
Task("hello", [Use("dev"), Run(["cowsay", "hello from kai"])]),
Build(
"greeting",
[
Use("dev"),
Run(["sh", "-c", "cowsay built by kai > greeting.txt"]),
Output("greeting.txt"),
],
),
Workflow("ci", [RunTask("hello", []), BuildArtifact("greeting")]),
])kai --help prints the header for the installed version: the platform and
std must come from the same release. Then:
kai check # compile and validate Kaifile.roc
kai update # pin package sources in .kai/lock.json
kai shell # enter the default shell
kai shell default -- cowsay hi # or run one command in it
kai run hello # run a task
kai run hello -- again # arguments after -- are appended
kai build greeting # sandboxed build; prints the store path
kai workflow ci # run steps in order, stop at a failureTool names are the backend's own package names: Nix attributes such as
python3, or Guix specifications. Tasks run their argv exactly, without a
shell, so use Run(["sh", "-c", "..."]) for pipes. Every command has help with
examples, e.g. kai run --help; inside a project, help lists its shells,
tasks, builds and workflows.
| Setting | Purpose |
|---|---|
Name(name) |
Project name. Required. |
Systems([...]) |
Systems the generated flake declares. Optional (default x86_64-linux and aarch64-linux); must include the system of the host Kai runs on. |
Packages(name, source) |
A package source: Auto, From(NixPackages(flakeRef)) or From(GuixPackages(...)). Tools use the default source (Auto: nixpkgs unstable on Nix, the installed channels on Guix) unless written "source#tool". |
Overlay(name, flakeRef) |
A Nix overlay, applied only where an environment selects it. |
Environment(name, [...]) |
A set of tools: Tools([...]), Overlays([...]) in order, and Extend(parent) to inherit the parent's tools and overlays first. |
Shell(name, [Use(environment)]) |
A shell for kai shell. |
Task(name, [Use(environment), Run(argv)]) |
A task for kai run. |
Source(name, flakeRef) |
A locked, non-flake source that builds can read. |
Build(name, [...]) |
A sandboxed Nix build of a project snapshot: Use(environment), Run(argv), a relative Output(path), and optionally Inputs([...]) (sources, in $KAI_INPUTS/<name>) and Needs([...]) (other builds, in $KAI_ARTIFACTS/<name>). |
Workflow(name, [...]) |
Steps for kai workflow: RunTask(task, args), BuildArtifact(build), RunWorkflow(workflow). |
Raw("nix", target, value) |
Extra attributes for the generated flake ("flake") or one shell ("shell:<name>"), e.g. Attrs([("shellHook", Str("echo hi"))]). |
Names and references are checked as the file compiles, so kai check reports
mistakes before anything runs. See examples for overlays, sources
and builds, and Guix.
Reuse is ordinary Roc: a module returns settings and Kaifile.roc imports it.
Shortened from examples/composition:
# ProjectTasks.roc
import std.Config
import std.EnvName
ProjectTasks :: [].{
settings : EnvName -> List(Config.Setting)
settings = |environment| [
Task("test", [Use(environment), Run(["git", "--version"])]),
]
}# Kaifile.roc, after the app header
import ProjectTasks
import std.Std
kaifile = Std.kaifile(
[
Name("composed"),
Environment("dev", [Tools(["git"])]),
Shell("default", [Use("dev")]),
].concat(ProjectTasks.settings("dev")),
)A module can only add settings. New commands, and replacements for one
command on one backend, come from plugins: Roc packages next to std in
Kaifile.new([...]). See docs/plugin.md and
examples/plugins/deploy, which adds kai deploy.
Kai picks a backend for each command. Nix is preferred when it is installed and
can serve the request. Guix is used when Nix is not installed, or when the
environment's tools come from a GuixPackages source. --backend nix or
--backend guix forces one; Kai never falls back to the other.
On Guix, kai shell and kai run run guix time-machine -C CHANNELS -- shell -q --pure with the environment's tools, at the Guix channel commit
Kai's lock pins. kai build runs guix build -f on a generated file whose
derivation runs the same sandboxed build runner as on Nix, and kai workflow
runs tasks and builds in order. kai update pins every installed backend that
fits, each in its own section of .kai/lock.json (kai --backend guix update
pins only Guix), and Guix commands refuse to run without a Guix pin. The first
Guix command at a new commit downloads, or builds, that Guix, which can take a
long time. Overlays and build inputs (Inputs([...])) need Nix. See
examples/channels.
kai update is the only command that resolves package sources. It writes
.kai/lock.json. kai shell, run, build and workflow only read it, and
ask you to run kai update when it is missing or a locked local source has
changed. Commit the lock; the rest of .kai is generated:
/.kai/*
!/.kai/lock.jsonKAI_DIR=<name> moves the whole directory to another top-level name in the
project.
-f,--file PATH: use another configuration file. Its directory is the project root.--json: print Kai's own messages as JSON Lines on stdout. Each hastypeandmessage, plus fields for its type (e.g.backend,step_started,artifact, orerrorwitherrorandexit_code). Output from shells and tasks passes through unchanged.--no-color, or a non-emptyNO_COLOR: plain text.--backend nix|guix: see Backends.ROC: the Roc compiler that evaluatesKaifile.roc(default:roc). It must be the pinned version; Kai says which one otherwise.
A failing child's exit status becomes Kai's. Usage errors exit 2 and other errors 1.
- Linux only. Kai runs on
x86_64-linuxandaarch64-linux, and generates shells and builds for the system it runs on. - Services, machines, deploy, switch, rollback, generations, images, ISOs and
secrets are not in this version, and the old
Kaifileformat is not read. If you need them, stay on v0.0.7 (nix run github:thebrandonlucas/kai/v0.0.7).
git clone https://github.com/thebrandonlucas/kai.git
cd kai
nix develop # or `direnv allow` once
zig build cizig build ci includes the end-to-end tests, which run a built kai against
real Nix and, when it is installed, Guix, so it needs network access or a warm
Nix cache. zig build e2e -- --nix or -- --guix runs them on one backend,
and zig build e2e-run (or -build, -workflow, -update, ...) runs one;
zig build guix-integration runs every Guix test and fails without Guix. If
nix develop is missing a dependency, please open an issue.
The code follows the pipeline:
platform: Kai's plugin platform, the Roc platformKaifile.rocbuilds on and the API every plugin uses: commands, backends, implementations, plans and the protocol kai speaks. It validates the Kaifile's plugins at compile time.plugins/std: the std plugin. It lowers its settings to its project model at compile time.plugins/std/model: std's project model and its validation.plugins/std/backends/nix,plugins/std/backends/guix: std's pure backends that plan files and argv from the model.
cli:kaiitself. It asks the compiled Kaifile for the command's candidate plans, chooses a backend, checks the plan and runs it.devtool: checks, integration tests and release tooling; see devtool/README.md and RELEASE.md.
Simple things should be simple, complex things should be possible
See design.md.
- Great UX. The benefits and usage of Kai should be immediate and obvious.
- Modularity: a. A great set of default features downstream of determinism: (rollbacks, dev shells, builds, garbage-collection, etc.) b. The ability to add/remove subcommands via a command module registry similar to Caddy. c. The ability to modify the default set of modules to fit user needs. d. To the degree possible, the ability to replace suboptimal pieces of the underlying system (i.e. encourage a "protocol" or modularity in determinate systems), as opposed to the current monolithic nature of Nix/Guix. See snix for example.
- Unlocking new use cases and ergonomics. Encouraging benefits that are overlooked or underutilized in current systems. Big examples would be easy desktop setups (or easily trying others' setups just to check them out!), easy, safe modification, easy backups etc. Simple examples include little ergonomic things like e.g.
kai shell keepto add any temporary shell programs to yourflake.nixpermanently (or eventually toconfiguration.nix).
If you would like to contribute, I would love for you to open an issue!
Aside from making a great tool for programmers to encourage the use of determinate computing, the hope is to go far beyond that and dream about what computers could be. I believe determinate computing is in its nascent form, and the true realization of its potential could have monumental and lasting effects as a new, better way to use computers.
Huge thank you to Luke Boswell for inspiring the initial portable typed configuration idea with roc-blueprint and his enthusiastic evangelism of this idea. The Kaifile platform, IR and Nix backend in kaifile/ began as roc-blueprint's code (see LICENSE).
Also thank you to the longstanding efforts of the Nix and Guix developers without which this would be impossible, the Roc team for their encouragement and making a great language to build in, and the caddy devs from which this project takes heavy inspiration.