gripsack icongripsack

gripsack — personal environment manager

your whole environment, in one bag.

set up a new machine from one repo — packages, dotfiles and machine-specific config, with generations and rollback.

v0.17.11 typed typescript rust core generations & rollback sandboxed eval no daemon · root · /nix alpha
$ curl -fsSL https://gripsack.dev/install.sh | sh linux · macOS

already installed? grip self-update · what's new

release binaries · the frontend is embedded in the binary — grip provisions its pinned, hash-verified Deno runtime automatically on first eval; npm i @gripsack/core only to pin your own copy

start with one dotfile

you don't have to rebuild your life. gripsack manages user-space tools and your configuration — it does not replace your OS package manager; brew or apt keep handling binaries, and package fetching is adopted later, tool by tool. the ownership question is the whole game:

adopting helix — nothing is touched until you say so
$ grip init my-env && cd my-env
$ grip adopt ~/.config/helix
adopting ~/.config/helix — 2 files, 1.1 kB

how should gripsack own these files?
  owned        read-only link — the repo is the only editor
❯ tracked_copy real file — the app may rewrite it; never clobbered
  merge        one managed block inside a shared file
# it asks, because only you know — the answer lands in modules/helix.ts
✓ how should gripsack own these files? owned
  wrote configs/helix/ · modules/helix.ts · hosts/laptop.ts
  ↻ ~/.config/helix/config.toml will be adopted (prior recorded)
apply? [y/N] y
applied — generation 1 active
$ grip rollback
rolled back to generation 0
# your original files have been restored — not deleted, restored

four ownership modes, because real dotfiles are messy: owned — a read-only symlink for disciplined tools; tracked_copy — a real copy for apps that rewrite their own config (Zed), with your edits detected, never overwritten; merge — a managed block inside a shared file like .bashrc; template — rendered from per-host vars. the full adopting guide.

see it happen

grip adopt — the ownership question, then a rollback that restores your files
grip adopt on an existing ~/.config/helix: it asks how to own the files, records prior state, applies, and grip rollback restores the original files byte-for-byte

re-rendered by CI from real demo runs — always the current CLI, in your palette.

treat your computer like code

[01]

describe

modules are typed TypeScript in your repo: sources, config files, ownership.

[02]

check

eval + semantic passes + 23 config linters, with spans pointing at the exact line. nothing stages on a red check.

[03]

plan

review the diff against the live generation before anything moves.

[04]

apply

one atomic flip into a new generation — a failed apply restores the previous managed state.

[05]

roll back

flip current back — every managed destination follows. that's the whole mechanism.

changing your machine should feel like changing code: typecheck it, inspect the plan, apply it atomically, roll it back.

a module

myenv — a module is a value, a host is a function, eval is sandboxed
// modules/helix.ts — a module is a pure exported value
import { module, githubRelease, symlink, trackedCopy } from "@gripsack/core";

export const helix = module("helix", {
  fetch: githubRelease({
    repo: "helix-editor/helix",
    asset: "helix-{version}-{system}.tar.xz",  // {version} + {system} — tag and host platform, expanded by the core
  }),
  install: { "bin/hx": symlink("~/.local/bin/hx") },
  config: {
    "config.toml": trackedCopy("~/.config/helix/config.toml"),
    "languages.toml": trackedCopy("~/.config/helix/languages.toml"),
  },
});

// hosts/laptop.ts — a host is a function; ctx carries facts and probes
import { defineEnv } from "@gripsack/core";
import { helix } from "../modules/helix.ts";
import cuda from "../modules/cuda.ts";

export default defineEnv((ctx) => ({
  tags: ["desktop"],
  modules: [
    helix,
    ctx.probe.executable("nvidia-smi") && cuda, // the core binds the probe; eval never runs it
  ],
}));

evaluation is sandboxed — no env vars, no network, no subprocesses. facts and probes arrive via ctx, injected by the core; effects are declared, never performed. eval emits IR (JSON) — the Rust core only ever consumes IR. the frontend is embedded in the binary and fully typed: your editor gives you autocomplete and inline errors, because a module is just typed code.

errors that point at your code

your dotfiles repo: helix needs its language servers, so it declares dep("lsp") — but you renamed that module pyright last week.

modules/helix.ts
import { dep, githubRelease, module, symlink } from "@gripsack/core";

export default module("helix", {
  fetch: githubRelease({ repo: "helix-editor/helix",
    asset: "helix-{version}-{system}.tar.xz" }),
  install: { "bin/hx": symlink("~/.local/bin/hx") },
  depends: [dep("lsp")],
});
grip plan
$ grip plan
error[E101]: module "helix" depends on unknown module "lsp"
  --> modules/helix.ts:7:14
   |
 7 |   depends: [dep("lsp")],
   |              ^ dependency declared here

every layer speaks the same diagnostic: eval, placeholders, paths, config linters, execution — stable codes, spans, suggestions, never a bare error: line. and because the passes are library functions over span-labeled IR, an LSP is a protocol shim, not new analysis.

what it does

01

dotfiles that don't fight their applications

your config files, managed per file: read-only links for disciplined tools, copies that never overwrite your edits, managed blocks in shared files. per-host variants pick files by machine facts — no templating language.

tracked_copy(…)
02

any source

install tools from anywhere: github releases, brew bottles, git, tarballs, conda — and anything with a weirder download dance as a small plugin. every byte is hash-checked. the fetchers.

github_release(…)brew(…)
03

generations

every apply is a complete new generation; switching is one atomic flip, and a failed apply rolls its partial deploys back. roll back by flipping to any previous one. a no-op apply creates nothing.

grip rollback
04

lockfiles, not sandboxes

inputs pinned by URL and content hash, per host. reproducible inputs, without requiring hermetic builds.

grip update

compare

gripsacknix + home-managerchezmoi · stowapt · brew · mise
manages your packages✓ any source·own repos
safely manages messy dotfiles✓ never clobbers; drift detectedwhole files onlytemplates·
rolls the managed environment back✓ generationsgit history·
config ownership modessymlink · tracked copy · managed block · templateno managed blocks, no drift detectiontemplates·
no custom languagetypescriptnix lang
reproducible inputslockfile pinsflakes + sandboxed builds (stronger)·
no daemon, no rootdaemon · /nixapt needs root
per-host conditionalsfacts + tagsmodulestemplates·
config linters, at eval, with spans···
fair is the point: Nix is the stronger guarantee. gripsack borrows one of its best ideas — generations — and stays a tool you can adopt one dotfile at a time.

install

release binaries · the frontend is embedded in the binary — the pinned Deno runtime provisions itself on first eval
then: grip init my-env to start fresh, or grip apply --repo git@github.com:you/myenv and you're home.

follow along

shipped so far: fetchers for github releases, brew bottles, git, tarballs, pixi — plus plugin fetchers over NDJSON, generations with rollback, and ownership modes that never clobber. latest: constrained eval — the frontend runs sandboxed under a pinned, hash-verified deno (no env vars, no network, no subprocesses), facts and probes arrive injected via ctx, and the first eval of an unfamiliar repo is an explicit trust decision. grip self-update — the package manager updates itself. what's new, every release. star the repo if this is the package manager you also wish existed.

pick a palette

pure CSS variables — the logo re-tints too. your pick persists per browser.