# ADR 0010: ccpm Package Manager

## Status

Accepted

## Date

2026-06-08

## Update (2026-06-15)

The beta/stable two-channel model has been retired now that `next` is merged into
`master`: `master` is the only channel. The `wget` bootstrap is renamed
`install-ccpm.lua` → `install-trapos.lua` (public entry point
`https://os.trapcloud.fr/install`) and now installs the **full OS** in one shot —
it bootstraps `ccpm`, then runs `ccpm update` + `ccpm install trapos` and a
`trapos-postinstall` welcome. Pass `--cpm-only` to stop after the ccpm bootstrap. The
persisted `branch` field is always `master`; the `--beta` opt-in and its confirmation
prompt are gone. The `upgrade` program is replaced by `trapos-upgrade` (`ccpm update` +
`ccpm upgrade`, with `--reboot`/`-r`). References to `--beta`/`install-ccpm.lua` below
are retained as historical context.

## Context

The previous install flow (a `LIST_FILES` table inside `install.lua` and a single flat
`manifest.json` read by `wget`) was all-or-nothing. There was no way to install just
networking or just the UI, and no way to add or remove pieces of the OS after the
initial install. The project also outgrew the "Trap's ComputerCraft APIs" framing into
a small in-game OS — TrapOS — with a name, a version, a boot banner, and a persisted
beta channel; that motion is preserved here even though the original install
mechanism is fully replaced.

We want a package manager, `ccpm` ("ComputerCraft Package Manager"), installed first
as a standalone user-facing step. After that, a machine can `ccpm update`,
`ccpm install trapos`, `ccpm install trapos-net`, `ccpm uninstall trapos-ui`, and manage
where packages come from. TrapOS itself is installed through a `trapos` meta-package;
the `wget run .../install-ccpm.lua` bootstrap exists only to install `ccpm`.

## Decision

### Packages are descriptors over the existing tree

Source files stay where they are (`apis/`, `programs/`, `servers/`, `startup/`); their
install targets remain the same absolute CC paths, so `require` paths and the dev
mounts are unchanged. A package is a descriptor that *references* those files:
`packages/<name>/ccpm.json` with `{ name, version, description, dependencies, files,
autostart }`. `packages/index.json` lists the packages a registry offers (for
`ccpm search`). There is no `ccpm.json` at the repo root.

The split is finer-grained than the install examples imply:

| package  | contents                                                        | deps     |
|----------|-----------------------------------------------------------------|----------|
| trapos-core | ccpm, libccpm, eventloop, upgrade, events                       | —        |
| trapos-test | libtest, runtest                                                | trapos-core |
| trapos-boot | motd, servers (startup)                                         | trapos-core |
| trapos-net  | net, router, ping, ping-server                                  | trapos-core |
| trapos-ui   | libtui, tuidemo                                                 | trapos-core |
| trapos-ai   | AI client for opencode serve                                    | trapos-core |
| trapos      | full TrapOS meta-package                                        | trapos-boot, trapos-net, trapos-ui, trapos-test, trapos-ai |

### Two files for ccpm, "manifest" reserved for the OS

To avoid colliding with the OS `manifest.json`, ccpm never uses the word "manifest".
Local state lives under `/trapos`:

- `ccpm.json` — ordered registry list `{ registries = { { name, type, branch } } }`.
  `type` is `gitea` (resolves to `git.trapcloud.fr/<name>/raw/branch/<branch>/`, the
  default seeded by the bootstrap) or `http`/`https` (the `name` is a base URL).
- `ccpm.lock.json` — installed packages `{ packages = { <name> = { version, registry,
  files, dependencies, autostart } } }`, used by `ls`, `uninstall`, and `reinstall`.
- `ccpm.cache.json` — packages advertised by configured registries, written by
  `ccpm update` from each registry's `packages/index.json`, used by `ccpm search`,
  `ccpm available`, and `ccpm upgrade`.

`apis/libccpm.lua` is the testable core (a factory; `http`/`stateDir`/`installRoot`
are injectable for tests). `programs/ccpm.lua` is a thin CLI over it.

### The bootstrap installs only ccpm

`install-ccpm.lua` resolves only the `trapos-core` package descriptor (pulling any future
dependencies), downloads its files, and writes:

- `/trapos/manifest.json` — the aggregated `{ name, version, branch, files, autostart }`
  still consumed by `startup/motd.lua` and `startup/servers.lua` after boot packages
  are installed. This is the surviving piece of the previous manifest-driven install:
  it is no longer the install source of truth (each package's `ccpm.json` is), but it
  is still the local system state used at boot for the colored `TrapOS v<version>`
  banner and to read the `autostart` list. `branch` is the persisted beta opt-in
  (a single confirmed `--beta` install switches subsequent `ccpm upgrade` runs to
  `next` with no flag needed; `--stable` is the symmetric opt-out);
- `/trapos/ccpm.lock.json` — so right after a fresh install `ccpm install trapos-core`
  correctly reports "already installed";
- `/trapos/ccpm.json` — seeding/refreshing the default `guillaumearm/cc-libs` registry
  to track the install branch.

The install path is:

- `wget run .../install-ccpm.lua` — install `ccpm` (`trapos-core`) and seed the default
  registry.
- `ccpm update` — refresh the local package cache.
- `ccpm install trapos` — install the full OS. During beta, `trapos` includes
  `trapos-test` by default.

On a subsequent `upgrade`, `programs/upgrade.lua` delegates to `ccpm upgrade`, which
upgrades installed packages using `/trapos/ccpm.cache.json`. Users run `ccpm update`
first to refresh available versions.

## Consequences

- The repo gains a `packages/` descriptor tree; the flat source layout is untouched.
- `just trapos` (formerly `just craftos`; see [ADR-0005](adr-0005-craftos-pc-harness-and-probes.md)) no longer derives mounts
  from `manifest.json .files` (it is now `.packages`); it mounts a fixed list of
  top-level dirs instead. `just test` was already on fixed mounts and is unaffected.
- ccpm logic is covered by `tests/ccpm.lua` (URL resolution, dependency ordering,
  cycle/missing detection, already-installed, registry CRUD, cache update, available
  status, upgrade, uninstall dependency guard) with an injected `http` stub — no
  network in tests.

## Future Work

- Version ranges (today a single pinned version per package).
- http/https registries beyond a plain base URL (auth, caching).
