# f-31: Support Multiple Versions in the Registry

- **Milestone:** M2 (post-MVP)
- **Complexity:** High
- **Depends On:** None

## 1. Problem Statement

Currently, the kley registry (`~/.kley/registry.json` + `~/.kley/packages/<name>/`) stores only the **latest** published version of each package. When a new version is published, the previous one is overwritten.

This creates two practical problems:

1. **`kley install` (no args) fails on version mismatch.** When `kley.lock` references version `1.0.0` but the registry already has `2.0.0`, `validate_version_in_registry` calls `process::exit(1)`. The user must manually intervene.
2. **No parallel development across projects.** If `project-a` needs `my-lib@1.x` and `project-b` needs `my-lib@2.x`, only one version can exist in the store at a time. This is a common scenario when developing a library that is consumed by multiple projects at different stages of migration.

## 2. Proposed Solution

Extend the registry to store multiple versions of the same package, similar to how npm's registry works.

### Directory Structure

```text
~/.kley/
  packages/
    my-lib/
      1.0.0/          ← version-scoped directory
        package.json
        index.js
      2.0.0/
        package.json
        index.js
```

Currently:
```text
~/.kley/
  packages/
    my-lib/           ← flat, single version
      package.json
      index.js
```

### Registry Data Changes

```json
{
  "packages": {
    "my-lib@1.0.0": {
      "lastUpdated": "2026-05-01T10:00:00Z",
      "installations": ["/projects/app-a"]
    },
    "my-lib@2.0.0": {
      "lastUpdated": "2026-05-11T12:00:00Z",
      "installations": ["/projects/app-b"]
    }
  }
}
```

Currently:
```json
{
  "packages": {
    "my-lib": {
      "version": "2.0.0",
      "lastUpdated": "...",
      "installations": ["/projects/app-a", "/projects/app-b"]
    }
  }
}
```

The `name@version` key format is consistent with the existing `package_name_version_parse()` function and keeps the `packages` map flat — one level of nesting, one struct for deserialization.

### Command Behavior Changes

| Command | Current Behavior | New Behavior |
|---------|-----------------|--------------|
| `kley publish` | Overwrites previous version | Adds `name@version` entry, keeps old ones |
| `kley install my-lib` | Installs latest | Installs latest version (highest semver among `my-lib@*` keys) |
| `kley install my-lib@1.0.0` | Fails if registry has 2.0.0 | Installs specific version |
| `kley install` (no args) | Fails on version mismatch | Installs exact version from `kley.lock` |
| `kley update my-lib` | Copies latest from store | Copies latest from store (unchanged) |
| `kley update my-lib@2.0.0` | N/A | Updates to specific version |
| `kley unpublish my-lib` | Removes entire package | Removes all `my-lib@*` entries and `packages/my-lib/` directory |
| `kley unpublish my-lib@1.0.0` | N/A | Removes specific entry and `packages/my-lib/1.0.0/` directory |

### Version Cleanup

Storing all versions indefinitely consumes disk space. A cleanup strategy is needed:

- **`kley clean`** already exists for stale registry entries — extend it to also remove `name@version` entries (and their `packages/<name>/<version>/` directories) that have no installations and are not the latest version.
- **Optional**: `kley unpublish my-lib@1.0.0` to remove a specific version entry and its directory.
- **Optional**: A max-versions config (e.g., keep last 3 versions per package).

## 3. Impact on f-19 (`kley install` no args)

Once multi-version is implemented, `kley install` with no args will:

1. Read `kley.lock` → get each package name and its locked version.
2. Call `install_package(name@version)` → `validate_version_in_registry` will find the exact `name@version` key in the registry.
3. No version mismatch, no crash — deterministic behavior matching `npm install`.

The current hard-fail on version mismatch is **intentional** and acts as a safety net until this ticket is implemented.

## 4. Migration

Existing single-version registry data must be migrated:

- Move `packages/<name>/` → `packages/<name>/<version>/` on disk
- Restructure `registry.json`: replace `"my-lib": { "version": "2.0.0", ... }` with `"my-lib@2.0.0": { "lastUpdated": "...", "installations": [...] }` — flat key per version
- `kley` should detect the old format on first run and auto-migrate

## 5. Acceptance Criteria

- `kley publish my-lib` when version 1.0.0 already exists creates a new directory `packages/my-lib/2.0.0/` without removing 1.0.0, and adds `my-lib@2.0.0` entry to the registry.
- `kley install my-lib@1.0.0` installs the specific version from `packages/my-lib/1.0.0/`.
- `kley install my-lib` (no version) installs the latest version (highest semver among `my-lib@*` keys).
- `kley install` (no args) installs the exact version from `kley.lock` without errors.
- `kley unpublish my-lib` removes all `my-lib@*` entries and the entire `packages/my-lib/` directory. `kley unpublish my-lib@1.0.0` removes only the specific entry and `packages/my-lib/1.0.0/`.
- `kley clean` removes `name@version` entries with zero installations (except the latest version per package) from both registry and disk.
- Registry data is automatically migrated from the old single-version format.
- Disk usage is reasonable — old versions without installations can be cleaned up.
