<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Fabrik Blog</title>
  <link href="https://fabrik.run/feed.xml" rel="self"/>
  <link href="https://fabrik.run/"/>
  <updated>2026-05-28T00:00:00Z</updated>
  <id>https://fabrik.run/</id>
  <author>
    <name>Fabrik</name>
  </author>
  
  <entry>
    <title>Hello, Fabrik</title>
    <link href="https://fabrik.run/blog/hello-fabrik/"/>
    <updated>2026-05-14T00:00:00Z</updated>
    <id>https://fabrik.run/blog/hello-fabrik/</id>
    <summary>What we are exploring, and why it lives at the seam between agents, code, and remote compute.</summary>
    <content type="html"><![CDATA[<p>Welcome to the Fabrik blog. We will use it for release notes, design decisions, and the occasional field note from the workshop.</p>
<p>Fabrik is our exploration of what automation looks like when an agent is on the other side of the interface. The graph has to be legible to something that does not read logs the way a person does, and the runtime has to reach past the box it was started on.</p>
<p>A laptop, a CI runner, a sandbox: each one is a small ceiling on what can be built, tested, or generated in a reasonable amount of time. We think the way through that ceiling is the network. Cache addressed by content, work executed on remote machines, results that come back as if they had been local all along. Once that path exists for code, it can carry the rest of the work a repository tends to grow: scripts, generators, datasets, deploys.</p>
<p>That is where we are pointing. We will write about what we learn as we get there.</p>
]]></content>
  </entry>
  
  <entry>
    <title>Remotely executable scripts</title>
    <link href="https://fabrik.run/blog/remotely-executable-scripts/"/>
    <updated>2026-05-27T00:00:00Z</updated>
    <id>https://fabrik.run/blog/remotely-executable-scripts/</id>
    <summary>Scripts that declare a contract can now run on remote compute through Fabrik. The first providers are Microsandbox and Daytona, and the door is open for more.</summary>
    <content type="html"><![CDATA[<p>Working with coding harnesses concurrently makes you notice your machine in a way you had not before. You spin up two, three, sometimes ten agents on <a href="https://git-scm.com/docs/git-worktree">git worktrees</a>, each one running tests, builds, lints, code generators. At some point you hear the fans. Then you watch the timings stretch. Your laptop has a number of cores, and those cores have a clock speed, and no amount of parallelism wishes that away. The box on your desk is the bottleneck.</p>
<p>The natural reaction is to move some of that work somewhere else. Not because remote is intrinsically better, but because at that scale of parallelism the laptop cannot keep up. You want the agent to keep running where it runs, looking at the code where you look at the code, but the compile-and-test loop it spawns ten times in a row could happen somewhere with more cores and more bandwidth.</p>
<h2>Build systems got there first, but kept it to themselves</h2>
<p>This is not a new idea. <a href="https://bazel.build">Bazel</a> has supported remote execution for a long time. So has <a href="https://buck2.build">Buck</a>. The build system describes each action with explicit inputs and outputs, hashes them, and either fetches a cached result or ships the action off to a remote worker. The local machine watches results come back. It works, and it has worked for years.</p>
<p>The catch is that the capability has been locked inside the build system. If your action is a Bazel rule, you get remote execution. If your action is a script you wrote last week to regenerate a fixture or run a test suite, you do not. Most repositories have a lot more of the second kind than the first.</p>
<p>For a while there was no obvious way out of that, because remote execution was expensive to build and expensive to operate. You needed an action protocol, a worker fleet, a content-addressed cache, and the willingness to run all of that in production. Bazel had it because Google had it. Most teams did not.</p>
<p>That is changing. The AI wave has poured money and attention into compute. Sandboxes, microVMs, runner companies, content-addressed storage, all of it has become much cheaper, much more accessible, much more standardized. The infrastructure that used to require a Google-scale team to operate is something you can now stitch together from open primitives. The question stops being <em>can we do remote execution</em> and starts being <em>what should be allowed to use it</em>.</p>
<h2>The narrow waist</h2>
<p>Fabrik exists to sit at exactly that boundary. We have <a href="/blog/2026-05-14-hello-fabrik/">written about this before</a>: Fabrik wants to be the narrow waist between coding agents and the infrastructure underneath them. Above the waist, the things developers and agents actually do. Build code. Run tests. Generate files. Ship the script that has been part of the repository for years. Below the waist, the infrastructure that makes those things faster. Caches. Sandboxes. Remote workers. Whatever compute providers are willing to expose next.</p>
<p>The reason a narrow waist matters is that without one, every above-the-waist thing has to know about every below-the-waist thing. Your test runner has to know about your cache. Your script has to know about your sandbox provider. Your codegen tool has to know about your CI runner. Multiply that by ten agents running in parallel and the whole thing collapses under the integration matrix.</p>
<p>A narrow waist flips the shape. Anything that can declare what it needs and what it produces becomes an action. Any action can be cached. Any action can be placed somewhere. The agent does not negotiate with the provider. The script does not know which sandbox is running. Fabrik handles the contract.</p>
<p><strong>Cacheable scripts were the first step. Remotely executable scripts are the second.</strong></p>
<h2>Placement is a user concern, not a repository concern</h2>
<p>A Fabrik script already declares its execution contract: inputs, outputs, environment, working directory. That contract is enough to make the script an action Fabrik can reason about, and once the action exists, that same action can be placed on a remote provider without changing the script.</p>
<p>Notice what the contract leaves out. The script does not say <em>where</em> it should run. The repository describes the work. The person, or the agent, running it picks where, in the same way a Git repository does not pick which remote you push it through.</p>
<p>So placement lives at the edge of the invocation. A repository can name a provider in the script contract when the work is known to need one, but the person or agent running the command can also override it from the CLI:</p>
<pre><code class="language-sh">fabrik run test --remote --compute daytona
fabrik exec --remote --compute daytona -- ./scripts/test.sh
</code></pre>
<p>The Daytona adapter talks to the Daytona API directly and points at an existing sandbox. In development that is just environment:</p>
<pre><code class="language-sh">export FABRIK_DAYTONA_SANDBOX=my-sandbox
export FABRIK_DAYTONA_API_KEY=...
export FABRIK_DAYTONA_WORKDIR=/workspace/fabrik
</code></pre>
<p>The first variable names the Daytona sandbox id or name. The second authenticates the API request, and can also be provided as <code>DAYTONA_API_KEY</code>. The third tells Fabrik where the repository root lives inside that sandbox, and defaults to <code>/workspace</code> when you do not set it.</p>
<p>For checked-in scripts, the repository can still carry the default:</p>
<pre><code class="language-sh"># FABRIK remote &quot;daytona&quot;
</code></pre>
<p>The script body does not change. The target does not grow provider-specific code. Fabrik routes the action through the selected provider, streams stdout and stderr back as it runs, restores declared outputs into the workspace, and caches the result. A second run with the same inputs, on your machine or on a teammate's, replays from the cache without touching the provider at all.</p>
<p>The streaming detail matters. If output only arrives after the provider finishes, the mental model shifts to job submission, and that is not the interface developers are used to with scripts. We want the experience to stay boring. You run the script. Output appears. The process exits. The fact that the compute happened in a Daytona workspace is a property of where, not of how.</p>
<h2>Two providers, room for many</h2>
<p>This first release ships with two providers.</p>
<p><a href="https://github.com/superradcompany/microsandbox">Microsandbox</a> is the one we use to exercise the whole flow end to end on a laptop. It is embedded in the Fabrik binary rather than treated as another executable on the path. Fabrik creates the microVM, mounts the repository, streams the process output, and tears the sandbox down when the command finishes. It does not give you more compute somewhere else, but it gives us a real provider contract that can run during development without asking people to stand up remote infrastructure first.</p>
<p><a href="https://www.daytona.io">Daytona</a> is the one we expect people to actually use. It gives you real remote workspaces with real horsepower, and it fits cleanly behind the same provider interface as Microsandbox. Point Fabrik at the sandbox, pass <code>--remote --compute daytona</code>, and the compile-and-test loop your agents are spawning ten times in a row stops being a fight against your laptop.</p>
<p>The provider interface is intentionally small. We do not think the remote execution market should be a one-vendor story, and we want adding a new provider to be straightforward enough that anyone with a use case can do it. If you would like Fabrik to support a provider we do not yet support, the contribution path is open. <strong>PRs are welcome.</strong></p>
<h2>What we are aiming at</h2>
<p>The real prize is not any single provider. It is the shape: a script that can describe itself, a contract that can travel, and a build system that can place actions where they belong without anyone above the waist having to think about it.</p>
<p>We will keep filling in the line. More providers. Smarter placement policies. Better observability for what is running where. The first contract is in. The interesting work begins now.</p>
]]></content>
  </entry>
  
  <entry>
    <title>Fabrik infrastructure</title>
    <link href="https://fabrik.run/blog/fabrik-infrastructure/"/>
    <updated>2026-05-27T00:00:00Z</updated>
    <id>https://fabrik.run/blog/fabrik-infrastructure/</id>
    <summary>Fabrik now has an infrastructure layer that lets the local build graph talk to external providers, starting with Tuist as a shared cache for builds and scripts.</summary>
    <content type="html"><![CDATA[<p>We started Fabrik with two ideas in mind: <strong>skipping work</strong> and <strong>distributing work</strong>. If a machine has already produced a result from a set of inputs, another machine should not have to repeat it. And if a piece of work can run somewhere better suited for it, like CI, a remote agent, or a faster machine close to the cache, the local workflow should be able to benefit from that without changing how the work is described. The boundary between laptop, CI, and agents is artificial. The inputs did not change. The action did not change. The fact that the work happened somewhere else should not force everyone to pay for it again.</p>
<p>Fabrik already models work as actions with explicit inputs, outputs, environment, working directory, and resource requirements. That gives us a content-addressed view of a task. If the action and its inputs are the same, the result should be reusable. Until now, that reuse lived in the local CAS. Useful, but still local. The next step was obvious: the cache should be able to live behind something else.</p>
<p>So we added the concept of infrastructure to Fabrik. The word matters because it says what this layer is trying to be. Not a feature hidden inside one command. Not a special integration baked into the runner. A small boundary where Fabrik can talk to external providers while keeping the core model stable. The build graph should not know if the cache is a directory on disk, a Tuist cache node nearby, or another provider we add later. It should ask for blobs and action results through the same interface and let the configured infrastructure decide where those bytes come from.</p>
<p>The first place where this shows up is caching. Fabrik now has a cache infrastructure abstraction, with the local CAS as the default and <a href="https://tuist.dev">Tuist</a> as one of the first remote infrastructures. The local cache still sits in front, so fast local hits stay fast, but successful actions can be mirrored to Tuist and later restored from another machine. We are not making Fabrik depend on Tuist as a hardcoded backend. We are giving Fabrik a way to connect to infrastructure, and Tuist happens to be the first one we care deeply about because it is where we are building low-latency caching for the developer workflows we know well.</p>
<p>The configuration is intentionally small. If your workspace wants to use Tuist directly, you can put the cache infrastructure in the root <code>fabrik.toml</code>:</p>
<pre><code class="language-toml">[infrastructure.cache]
kind = &quot;tuist&quot;
account = &quot;acme&quot;
</code></pre>
<p>By default Fabrik talks to <code>https://tuist.dev</code>. If you self-host, you can make that explicit:</p>
<pre><code class="language-toml">[infrastructure.cache]
kind = &quot;tuist&quot;
url = &quot;https://tuist.dev&quot;
account = &quot;acme&quot;
</code></pre>
<p>There is also a named-provider form, which is the one we expect to age better for teams. A repository can say which provider it wants, and each developer or machine can define that provider once in the user config. That keeps repository configuration focused on the project scope, while the details of the infrastructure live at the system level:</p>
<pre><code class="language-toml"># fabrik.toml
[infrastructure.cache]
name = &quot;tuist&quot;
account = &quot;acme&quot;
</code></pre>
<pre><code class="language-toml"># ~/.config/fabrik/config.toml
[infrastructures.tuist]
kind = &quot;tuist&quot;
url = &quot;https://tuist.dev&quot;
</code></pre>
<p>Authentication follows the same idea. It should be a system concern, not something every repository invents again. On a developer machine, Fabrik can authenticate once and reuse that session through the system credentials directory:</p>
<pre><code class="language-sh">fabrik auth login --provider tuist
</code></pre>
<p>That login is not tied to one checkout. It is tied to the provider identity and stored under the user's Fabrik configuration area. The point is the same as with Git credentials or cloud CLIs. You should not have to teach every project how to authenticate with the same service. You authenticate with the service once, and then every workspace that resolves to that infrastructure can use it. If a repository says <code>name = &quot;tuist&quot;</code> and your user config points that name to Tuist, the build command does not need another flag. It resolves the infrastructure, loads the stored session, and goes through the same cache interface as the local provider.</p>
<p>Where this gets more interesting is scripts. Scripts are one of those humble pieces of infrastructure that every project has, but that almost no build system treats well. They are usually invisible to caching because they live outside the formal graph. Fabrik has been moving in a different direction: a script can describe its execution contract in the file itself, and once that contract is explicit, the script becomes cacheable like anything else.</p>
<p>For example, imagine a small script that turns an input file into an output file:</p>
<pre><code class="language-sh">#!/usr/bin/env -S fabrik exec -- bash
# FABRIK input &quot;../input.txt&quot;
# FABRIK output &quot;../out.txt&quot;
# FABRIK cwd &quot;..&quot;

tr '[:lower:]' '[:upper:]' &lt; input.txt &gt; out.txt
cat out.txt
</code></pre>
<p>You can commit that as <code>scripts/uppercase.sh</code>, make it executable, and run it directly:</p>
<pre><code class="language-sh">chmod +x scripts/uppercase.sh
./scripts/uppercase.sh
</code></pre>
<p>The first run is a miss. Fabrik sees the script path, the declared input, the working directory, the command line, and the declared output. It runs the script, stores stdout, stderr, and <code>out.txt</code>, and publishes the action result through the configured cache infrastructure. If the workspace is configured with Tuist, the local CAS gets the result immediately, and Tuist can receive it for other machines. The second run on your machine becomes a local hit. A teammate or an agent running in another environment can get a Tuist hit and restore the output without rerunning the script.</p>
<p>You can also expose the same script as a target if you prefer the workspace graph to name it:</p>
<pre><code class="language-toml">[[target]]
name = &quot;uppercase&quot;
rule = &quot;script&quot;

[target.script]
path = &quot;scripts/uppercase.sh&quot;
</code></pre>
<p>Then the command becomes:</p>
<pre><code class="language-sh">fabrik run uppercase
</code></pre>
<p>This is a small example, but it points at the shape of the system. A lot of developer automation starts as a script because scripts are easy to write and easy to understand. The problem is that they often remain opaque forever. By letting scripts declare their inputs and outputs, Fabrik gives them enough structure to participate in the cache without forcing every team to rewrite them as a plugin, a rule, or a custom task type. And by letting the cache provider be Tuist, the result is not trapped on one laptop.</p>
<p>There is a larger thread here that connects to what we are building at <a href="https://tuist.dev">Tuist</a>. Compute is becoming more fluid. Your build might run locally, in <a href="https://github.com/features/actions">GitHub Actions</a>, in <a href="https://openai.com/codex/">Codex</a>, in a <a href="https://www.coder.com/">remote VM</a>, or somewhere we are not thinking about yet. If every environment starts from zero, we lose. If the useful work can travel through a shared cache, we get a system where the location of compute matters less and the correctness of the action model matters more.</p>
<p>Fabrik infrastructure is a step in that direction. The local model stays simple. The external provider stays swappable. Authentication is shared at the system level. Scripts get to become first-class cached actions without losing their simplicity. It is not the whole story, but it is the kind of foundation we like: small enough to understand, explicit enough to trust, and open enough that the next provider does not require changing how people describe their work.</p>
]]></content>
  </entry>
  
  <entry>
    <title>The cache, at runtime</title>
    <link href="https://fabrik.run/blog/cache-at-runtime/"/>
    <updated>2026-05-28T00:00:00Z</updated>
    <id>https://fabrik.run/blog/cache-at-runtime/</id>
    <summary>Fabrik now exposes its content-addressed cache as a small set of CLI commands so any script can check, store, and restore results without going through the build graph.</summary>
    <content type="html"><![CDATA[<p>The cache inside a build system is usually only visible to the build system. The graph decides what counts as an action, the runner hashes its inputs, and the result lives in a store nobody else can reach. That works for the actions the build system already knows about. It does not help with everything else, which in most repositories is a lot.</p>
<p>A repository tends to grow a long tail of scripts that exist next to the build. Test runners. Codegen. Dependency installs. Environment bootstraps. They are the moments where a developer or an agent says &quot;do this thing first, then move on&quot;. They are also, very often, exactly the moments where a cache would matter most, because the inputs barely changed and the work is about to run again.</p>
<h2>What we shipped</h2>
<p>Fabrik already used a content-addressed cache for the actions it executes. With this work, that cache is reachable from any shell. A handful of commands, all under <code>fabrik cache</code>, do the small set of things a script needs to participate in caching on its own:</p>
<pre><code class="language-sh">fabrik cache blob put &lt;path&gt;              # store bytes, print the content digest
fabrik cache blob get &lt;digest&gt;            # fetch a blob by content digest
fabrik cache blob exists &lt;digest&gt;         # exit 0 on hit, 1 on miss
fabrik cache action get --input &lt;spec&gt;... # look up a result by declared inputs
fabrik cache action put --input &lt;spec&gt;... # record a result
fabrik cache action forget &lt;digest&gt;       # drop a result
fabrik cache stats                        # counts and size for each cache
</code></pre>
<p>Two caches. The shape mirrors what Bazel and the <a href="https://github.com/bazelbuild/remote-apis">Remote Execution API</a> settled on. The <strong>blob cache</strong> stores bytes addressed by their <a href="https://github.com/BLAKE3-team/BLAKE3">BLAKE3</a> hash; a <code>get</code> always returns bytes that hash back to the digest you asked for. The <strong>action cache</strong> maps an action digest, which Fabrik derives from whatever inputs you declare, to an <code>ActionResult</code>: an exit code, optional stdout and stderr digests, and any declared outputs as <code>path -&gt; blob digest</code>. Output digests point back into the blob cache by content, so an action result is a small proto and the bytes live in one place.</p>
<h3>Declaring inputs</h3>
<p>Most commands accept inputs that describe what determines a result. The grammar is small and the same everywhere it appears:</p>
<ul>
<li><strong><code>&lt;path&gt;</code></strong>: a file or directory (directories walk sorted, content plus relative path).</li>
<li><strong><code>path:&lt;path&gt;</code></strong>: explicit path form for names that contain <code>:</code>.</li>
<li><strong><code>value:&lt;str&gt;</code></strong>: a literal string.</li>
<li><strong><code>env:&lt;NAME&gt;</code></strong>: an environment variable, hashed as <code>&lt;NAME&gt;\0&lt;value&gt;</code> so two variables sharing a value do not collide.</li>
<li><strong><code>-</code></strong>: standard input.</li>
</ul>
<p>So when you write <code>--input src --input vitest.config.ts --input env:NODE_ENV</code>, Fabrik walks the <code>src/</code> tree sorted, hashes the config file's bytes, reads <code>NODE_ENV</code> from the environment, combines the three digests in order, and uses that as the action key. You never see a hash unless you ask for one.</p>
<h2>Why expose this at all</h2>
<p>Build systems already do this internally. The reason we want it at the level of a script is that the boundary of &quot;what the build system knows about&quot; is artificial. A pre-commit hook, a test runner, a deploy script, a fixture generator: all of them are workflows that produce a result from a set of inputs. They deserve the same skip-if-unchanged behavior. Forcing teams to wrap every one of these as a custom rule or a plugin is a tax we do not think is necessary.</p>
<p>Once the cache is a primitive, scripts can do interesting things on their own. Control flow becomes a first-class user of the cache. If a previous run with the same inputs already succeeded, the script can decide to exit early. If a previous run produced an artifact, the script can restore it instead of regenerating it. The script stays a script, and the speedup comes for free from the same store that the build graph uses.</p>
<h2>Skipping a Vitest run when nothing changed</h2>
<p>A test suite is the classic case. The result of running <a href="https://vitest.dev">Vitest</a> over a clean tree depends on the source, the tests, the config, and the lockfile. If none of those changed since the last green run, there is no reason to run them again.</p>
<p>What we want to remember is whether a <em>run</em> succeeded, not an artifact, so the action cache is the right fit:</p>
<pre><code class="language-sh">#!/usr/bin/env bash
set -euo pipefail

inputs=(
  --input src
  --input test
  --input vitest.config.ts
  --input pnpm-lock.yaml
)

# `--if-success` exits 0 only when the cache has a record AND the
# recorded exit code is 0. A cached failure or a miss exits non-zero,
# and the tests run.
if fabrik cache action get &quot;${inputs[@]}&quot; --if-success; then
  echo &quot;vitest: cached green run for these inputs, skipping.&quot;
  exit 0
fi

pnpm vitest run

# Record success. `put` defaults --exit-code to 0; `set -e` would have
# exited above on failure, so the only path that reaches this line is
# a green run.
fabrik cache action put &quot;${inputs[@]}&quot;
</code></pre>
<p>Run the script once and the tests execute. Run it a second time without touching the inputs and the script exits immediately. Change a single character in any file under <code>src</code>, <code>test</code>, <code>vitest.config.ts</code>, or <code>pnpm-lock.yaml</code>, and the input digest changes, the cache misses, the tests run again.</p>
<p>You can extend the same shape to other test runners, linters, type checkers, anything whose result is a function of a set of files.</p>
<h2>Restoring node_modules without reinstalling</h2>
<p>The other shape this enables is restoring an artifact instead of regenerating it. <code>npm install</code> is the canonical example. It is slow, the inputs are very stable, and the output is a folder you could have copied in a fraction of the time.</p>
<p>The recipe is the same primitive at work, with one extra step: the artifact you want to remember lives in the blob cache, and the action result you record under the input digest points at it.</p>
<pre><code class="language-sh">#!/usr/bin/env bash
set -euo pipefail

inputs=(--input package.json --input package-lock.json)

# If we recorded a result for these inputs, restore the tarball.
result=$(fabrik cache action get &quot;${inputs[@]}&quot; --format json)
if echo &quot;$result&quot; | grep -q '&quot;hit&quot;:true'; then
  digest=$(echo &quot;$result&quot; | jq -r '.result.outputs[&quot;node_modules.tar&quot;]')
  fabrik cache blob get &quot;$digest&quot; | tar -xf -
  echo &quot;node_modules: restored from cache.&quot;
  exit 0
fi

# Cache miss: install, store the tarball in the blob cache, and
# record an action result pointing at it.
npm install
nm_digest=$(tar -cf - node_modules | fabrik cache blob put)
fabrik cache action put &quot;${inputs[@]}&quot; \
  --output node_modules.tar=&quot;$nm_digest&quot;
</code></pre>
<p>Three operations carry the whole flow: probe the action cache, put the tarball in the blob cache, record the result. Because the tarball is content-addressed, two teammates whose installs produce byte-identical bytes end up sharing one entry; the second teammate's <code>cache blob put</code> recognises the digest and the bytes are not duplicated.</p>
<p>The same pattern works for <code>pip install</code>, <code>bundle install</code>, <code>cargo fetch</code>, any output a tool produces deterministically from a small set of input files. The script does the wrapping. The cache does the heavy lifting.</p>
<p>The cross-machine version, where the first developer to install pays the cost and every teammate and CI runner after them restores the tarball through <a href="https://tuist.dev">Tuist</a>, already works for the blob cache; the action-cache side of the same shape lands as that integration matures.</p>
<h2>A mise toolchain that follows you between branches</h2>
<p><a href="https://mise.jdx.dev">mise</a> is a popular runtime version manager. Switching branches whose <code>mise.toml</code> declares different tool versions kicks off a fresh round of downloads even when you have already paid for them on another branch or another machine. The <a href="https://github.com/jdx/mise-action">mise-action</a> for GitHub Actions exists for exactly this reason: it caches <code>~/.local/share/mise/</code> (the binary plus every installed tool) under a key derived from the platform, the mise version, and the mise config and lockfile, and restores it on the next run.</p>
<p>The shape isn't GitHub-specific. The same script gives a single dev machine, an agent's worktree, and a CI runner the same speedup, and on a Tuist-backed blob cache the toolchain follows you between machines too.</p>
<pre><code class="language-sh">#!/usr/bin/env bash
set -euo pipefail

# Platform discriminator first: a mise tree built on macOS arm64 cannot
# run on Linux x86_64. `value:` carries a cache-format version you bump
# when the layout itself changes.
inputs=(
  --input value:mise-v1
  --input env:OSTYPE
  --input env:HOSTTYPE
  --input mise.toml
  --input mise.lock
)

mise_dir=&quot;${MISE_DATA_DIR:-$HOME/.local/share/mise}&quot;

result=$(fabrik cache action get &quot;${inputs[@]}&quot; --format json)
if echo &quot;$result&quot; | grep -q '&quot;hit&quot;:true'; then
  digest=$(echo &quot;$result&quot; | jq -r '.result.outputs[&quot;mise.tar&quot;]')
  mkdir -p &quot;$mise_dir&quot;
  fabrik cache blob get &quot;$digest&quot; | tar -xzf - -C &quot;$(dirname &quot;$mise_dir&quot;)&quot;
  echo &quot;mise: restored toolchain from cache.&quot;
else
  mise install --locked
  tools_digest=$(
    tar --sort=name -czf - -C &quot;$(dirname &quot;$mise_dir&quot;)&quot; &quot;$(basename &quot;$mise_dir&quot;)&quot; \
      | fabrik cache blob put
  )
  fabrik cache action put &quot;${inputs[@]}&quot; --output mise.tar=&quot;$tools_digest&quot;
fi
</code></pre>
<p>The pattern generalises: anything that mutates a known directory deterministically from a small set of inputs (a virtualenv built from <code>requirements.txt</code>, a Bundler gem set, a sandboxed npm workspace, a <code>cargo build</code> target directory) fits the same three commands.</p>
<h2>A primitive, not a feature</h2>
<p>The reason we like this shape is that we do not have to anticipate what people will use it for. A cache addressable from the shell is a primitive. The CI bootstrap script that does ten things in a row can wrap each of them. The fixture generator that takes a minute can become a one-liner the second time. The agent that runs the same workflow ten times across ten worktrees can stop redoing the same work in parallel.</p>
<p>If you build something on top of these commands, or you wish they did one more thing to make your workflow possible, <a href="https://github.com/tuist/fabrik">open an issue or a discussion in the repository</a>. We are collecting use cases as we go, and the next round of work on the cache surface will be informed by what people are actually trying to skip.</p>
]]></content>
  </entry>
  
</feed>
