Metadata-Version: 2.4
Name: anchored-install
Version: 0.1.0
Summary: Backend-agnostic installer with Dockerfile-style layer caching and resumable installs.
Project-URL: Repository, https://github.com/OpenGHz/anchored-install
Author: OpenGHz
License: MIT
License-File: LICENSE
Keywords: bootstrap,cache,dockerfile,idempotent,install,installer,resume
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Installation/Setup
Classifier: Typing :: Typed
Requires-Python: >=3.8
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Description-Content-Type: text/markdown

# anchored-install (`ai`)

English | [简体中文](README.zh-CN.md)

**A backend-agnostic installer with Dockerfile-style layer caching and resumable installs.**

> *Why "anchored"?* Every step that succeeds is dropped as an **anchor** — a
> checkpoint you can't drift back past. A failed re-run resumes from the last
> anchor instead of casting off from chapter one.

Write your install steps as a plain list of shell commands. `anchored-install` runs
them one by one and **caches each step that succeeds**. When a run fails, fix
the problem and re-run — already-succeeded steps are skipped and execution
**resumes from the first unfinished step**, instead of starting over from
chapter one every time.

- ✅ **Resumable** — a failed reinstall doesn't repeat everything that already worked.
- ✅ **No "is it already installed?" checks** — the cache *is* the state.
- ✅ **Backend-agnostic** — each line is just a command handed to your shell; if the command works on this machine, `ai` can run it.
- ✅ **It's a normal script** — keep the native extension (`.sh` on Linux). The `#ai` markers are plain comments, so `bash install.sh` runs the exact same file natively, with or without `ai`.
- ✅ **Native & portable** — pure Python standard library, **zero dependencies**.

> Tested on Ubuntu with bash. Other shells/platforms are a planned extension.

## Install

```bash
pip install anchored-install        # provides the `ai` command (and `anchored-install`)
# or, from a checkout:
pip install -e .
```

Requires Python ≥ 3.8 and a POSIX shell (`/bin/bash` by default).

## Quick start

Write a manifest (e.g. `install.sh` — use the native shell extension) — it's just
commands, one logical line per step, and runs as a normal bash script too:

```bash
echo "==> setting up"
export BUILD_DIR=/opt/myapp
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"

# Multi-line constructs (if / for / heredoc): wrap them in a block.
#ai >>>
if ! command -v node >/dev/null; then
  curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
  sudo apt-get install -y nodejs
fi
#ai <<<

git clone https://github.com/me/myapp . || git pull
npm ci

# This step runs every time, even when everything else is cached.
#ai: always
echo "installed at $(date)"
```

Run it:

```bash
bash install.sh        # plain native run, no caching (the #ai markers are comments)

ai run install.sh      # execute with caching; cached steps are skipped
ai status install.sh   # show which steps are cached vs pending
ai plan install.sh     # dry-run: parsed steps + cache keys, nothing executed
ai clean install.sh    # forget cached progress for this manifest
```

If a step fails, `ai` stops and tells you where. Fix it, run `ai run install.sh`
again, and it picks up exactly where it left off.

> The file keeps its native extension (`.sh` on Linux) and stays a valid shell
> script — `ai` adds caching/resume on top without a special file format.

## How it works

### Steps
- Each non-comment **logical line** is one step (`\` continues a line).
- Comments (`# ...`) and blank lines are ignored and **never affect the cache** —
  reformatting comments won't bust anything.
- `#ai >>>` … `#ai <<<` collapses several lines into **one** step (for `if`/`for`/heredocs).
- `#ai: always` marks the next step to run on every invocation; `#ai: name <label>` labels it.

### Layer caching (chained hashes)
Each step's cache key folds in the previous step's key:

```
key[i] = sha256(key[i-1] + normalized(step[i].text))
```

So editing step *i* invalidates step *i* **and everything after it**, while
steps `0..i-1` stay cached — exactly Docker's layer model. Appending new steps
at the end leaves all earlier steps cached.

### Environment carries across steps (and across cached skips)
Steps run in separate shell invocations, yet `cd` and `export` still persist:
after each successful step `ai` snapshots the **delta** it made to the
environment and working directory (a Docker-like layer). When a cached step is
skipped, its delta is replayed, so later steps still see the right directory and
variables — even ones set by steps that didn't re-run.

Cache lives under `~/.cache/anchored-install/<manifest-id>/` (`0700`), one snapshot
file per succeeded step.

## CLI reference

```
ai run    <file> [--no-cache] [--from N] [--workdir DIR] [--shell PATH] [--no-env-snapshot]
ai status <file>
ai plan   <file>
ai clean  <file>
```

- `--no-cache` — ignore existing cache for this run (still records fresh results).
- `--from N` — force re-run from step *N* (1-based) onward.
- `--workdir DIR` — starting directory (default: the manifest's own directory).
- `--shell PATH` — shell used to run steps (default: `$AI_SHELL` or `/bin/bash`).
- `--no-env-snapshot` — run each step as an independent subprocess; `cd`/`export` no longer carry across steps.

## Caveats & trade-offs

- **Success is trusted.** If a step's external effect is undone by hand (you
  manually removed an installed package), `ai` still considers it done. Use
  `ai clean` or `ai run --no-cache` to rebuild.
- **`#ai: always` steps re-run, but their text is unchanged**, so their cache key
  (and downstream keys) don't change — downstream steps may still be served from
  cache. Use `always` for effects that don't gate later steps (logging, banners).
- **Env snapshots touch disk.** They can contain exported secrets; the cache dir
  is created `0700`. Use `--no-env-snapshot` if you'd rather not persist them.
- A standalone step whose final command is a failing `&&`/`||` list (e.g.
  `grep x file` with no match) is treated as a **failure** — append `|| true` if
  that's intentional.

## Not yet (planned extensions)
Parallel steps, remote/container backends, Windows/PowerShell, manifest
`include` and variable templating. The parser/executor are layered to make these
additive.

## Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) to get
set up, and note the [Code of Conduct](CODE_OF_CONDUCT.md). For questions see
[SUPPORT.md](SUPPORT.md); to report a vulnerability see [SECURITY.md](SECURITY.md).
Release notes live in [CHANGELOG.md](CHANGELOG.md).

## License
MIT © OpenGHz — see [LICENSE](LICENSE).
