Imported from expelledboy/tix (
AGENTS.md). Install upstream withnpx skills add expelledboy/tix. Copyright stays with the author.
AGENTS.md
This file provides context for AI coding assistants (Cursor, GitHub Copilot, Claude Code, OpenClaw, etc.) working with the Tix repository.
Project Overview
Tix is a tiny, TypeScript reimagining of Nix — content-addressed builds without the DSL.
- Repository: https://github.com/expelledboy/tix
- License: MIT
What Tix Does
Tix brings Nix's core ideas to TypeScript:
- Content-addressed store:
/tix/store/<hash>-<name> - Reproducible builds: Same inputs → same hash → same output
- Sandboxed execution: Docker-based isolation
- Declarative: TypeScript is the configuration language
What Tix Is NOT
- Not a Nix replacement (much simpler, fewer features)
- Not production-ready yet (0.x version)
- Not compatible with Nix store or nixpkgs
Repository Structure
tix/
├── src/
│ ├── core/ # Core implementation (low-level)
│ │ ├── types.ts # Type definitions, branded types
│ │ ├── hash.ts # SHA256, Nix32, store path computation
│ │ ├── store.ts # Content-addressed store
│ │ ├── derivation.ts # Hash algorithm, instantiation
│ │ ├── build.ts # Docker sandbox execution
│ │ └── index.ts # Core exports
│ ├── api.ts # User-facing API (sh, drv, fetchUrl, env)
│ ├── cli.ts # CLI entry point
│ ├── index.ts # Public exports
│ └── __tests__/ # Test files
├── examples/ # Usage examples
├── docs/ # Documentation and research notes
├── dist/ # Build output (gitignored)
└── coverage/ # Test coverage (gitignored)
Development Setup
Requirements
- Node.js: >=18
- pnpm: Package manager
- Docker: For sandboxed builds (optional for tests)
With Nix (recommended)
nix develop
Without Nix
pnpm install
Development Commands
| Command | Description |
|---|---|
pnpm build |
Compile TypeScript to dist/ |
pnpm dev |
Watch mode compilation |
pnpm test |
Run all tests |
pnpm test -- --coverage |
Run tests with coverage |
pnpm test -- --watch |
Watch mode testing |
pnpm lint |
Check formatting (Prettier) |
pnpm format |
Fix formatting |
Running After Changes
Always run these before committing:
pnpm build # Ensure it compiles
pnpm test # Ensure tests pass
pnpm lint # Ensure formatting is correct
Architecture
Layer Diagram
┌─────────────────────────────────────────────────────┐
│ User Code: import { sh, drv, build } from 'tix' │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ src/api.ts — High-level API │
│ • sh`` template literal │
│ • drv() explicit derivation │
│ • fetchUrl() fixed-output │
│ • build() / outPath() / show() │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ src/core/ — Low-level implementation │
│ • types.ts: Branded types (StorePath, DrvPath) │
│ • hash.ts: Nix32 encoding, SHA256, fingerprints │
│ • derivation.ts: hashDerivationModulo, instantiate │
│ • store.ts: Content-addressed storage │
│ • build.ts: Docker sandbox execution │
└─────────────────────────────────────────────────────┘
Key Concepts
- Derivation (user-facing): What the user defines — name, builder, args, inputs
- DrvFile (stored): What gets written to the store as JSON
- hashDerivationModulo: The recursive hash algorithm that makes it all work
- Store: Content-addressed, immutable file storage
The Hash Algorithm
This is the core insight from Nix:
function hashDerivationModulo(drv: Derivation): Hash {
// Fixed-output: hash based on OUTPUT content, not inputs
if (drv.outputHash) {
return sha256(`fixed:out:sha256:${drv.outputHash}:`);
}
// Regular: hash includes ALL inputs recursively
const inputHashes = drv.inputs.map(i => hashDerivationModulo(i));
return sha256({ name, builder, args, env, inputs: inputHashes });
}
This means:
- Change any input → different hash → different output path
- Fixed-output derivations are identified by their content
Coding Standards
TypeScript
- Strict mode: Enabled, do not disable
- ES Modules: Use
import/export, neverrequire() - Branded types: Use
StorePath,DrvPath,Hash— don't use plain strings
Formatting
- Tool: Prettier
- Config:
.prettierrc - Settings: Single quotes, trailing commas, 2-space indent, 100 char lines
- Run:
pnpm formatbefore committing
File Naming
- Source files:
kebab-case.ts - Test files:
*.test.tsin__tests__/directory
Testing
- Framework: Jest with ts-jest
- Location:
src/__tests__/*.test.ts - Pattern: One test file per module (hash.test.ts, store.test.ts, etc.)
Test categories:
hash.test.ts: Hashing, Nix32 encoding, deterministic JSONstore.test.ts: Store operations, atomic writes, immutabilityderivation.test.ts: hashDerivationModulo, topoSort, instantiateapi.test.ts: User API (sh, drv, fetchUrl, env)integration.test.ts: End-to-end scenarios
Error Handling
- Throw descriptive
Errorwith context - Include the derivation name in errors
- For cycles, include the full path:
a -> b -> c -> a
Important Implementation Details
Nix32 Encoding
Nix uses a custom base32 that:
- Omits letters e, o, u, t (avoid confusion with 0, 1)
- Reverses bytes before encoding
- Outputs characters in reverse order
// Alphabet: 0123456789abcdfghijklmnpqrsvwxyz (no e, o, u, t)
const reversed = Buffer.from(bytes).reverse(); // Critical!
Store Path Format
/tix/store/<hash>-<name>
hash = nix32(sha256(fingerprint)[0:20]) // 32 chars
fingerprint = "output:out:sha256:<drvHash>:<storeDir>:<name>"
Branded Types
We use branded types for type safety:
type StorePath = string & { readonly __brand: "StorePath" };
type DrvPath = StorePath & { readonly __brand: "DrvPath" };
When you need string methods, cast: (path as string).split('/')
Atomic Writes
All store writes must be atomic:
- Write to temp file in same filesystem
rename()to final path (atomic on POSIX)- Set read-only permissions
Common Tasks
Adding a New API Function
- Add to
src/api.ts - Export from
src/index.tswith JSDoc - Add tests in
src/__tests__/api.test.ts - Add example in
examples/
Modifying the Hash Algorithm
⚠️ Be extremely careful — this affects all store paths.
- Understand the Nix spec first (see
docs/RESEARCH.md) - Add tests BEFORE changing code
- Verify against real Nix output if possible
- Update
docs/RESEARCH.mdwith any findings
Adding a Test
describe('featureName', () => {
it('does specific thing', () => {
const result = functionUnderTest(input);
expect(result).toBe(expectedOutput);
});
});
For tests needing temp directories:
let tempDir: string;
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'tix-test-'));
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});
Do NOT
- Use
JSON.parse()directly on untrusted input - Disable TypeScript strict mode
- Use
anytype (useunknownand narrow) - Commit without running tests
- Change hash algorithm without extensive testing
- Add dependencies without good reason (keep it tiny)
- Use
rm -rfin code (usermSyncwith proper paths)
Changesets
When making changes that affect the published package:
- Run
pnpm changeset - Select
tixpackage - Choose version bump type:
patch: Bug fixes, internal changesminor: New features (backwards compatible)major: Breaking changes
- Write a clear description for release notes
References
- Nix PhD Thesis — The foundational document
- Nix Pills — Practical walkthrough
- Store Path Spec
docs/RESEARCH.md— Our notes on Nix internals
Getting Help
- Check existing tests for examples
- Read
docs/RESEARCH.mdfor Nix algorithm details - Open an issue for questions