parley

Description

Smalltalk packages, resolved by conversation.

Details

Source
GitHub
Dialect
gnu_smalltalk (40% confidence)
License
MIT
Created
July 18, 2026
Updated
Aug. 16, 2026
Topics
dependency-resolution dependency-resolver gnu-smalltalk package-manager semver smalltalk

README excerpt

# Parley

[![verify](https://github.com/leocamello/parley/actions/workflows/verify.yml/badge.svg)](https://github.com/leocamello/parley/actions/workflows/verify.yml)

> *Smalltalk packages, resolved by conversation.*

Parley is a modern, native command-line package manager for [GNU Smalltalk](https://www.gnu.org/software/smalltalk/) 3.2.5, written entirely in Smalltalk.

The language is called Smalltalk — casual conversation. A **parley** is the formal one: a negotiation between independent parties to resolve a conflict. And that is exactly what dependency resolution is — a negotiation between constraints, ending either in agreement (a `Resolution`) or a documented account of why the parties couldn't agree (a `ConflictReport`, rendered as a narrated transcript of the negotiation).

## Status

**v1.0, and the loop closes in both directions.** An author can publish a package into an index — flat, or the per-package sparse layout a client reads without ever downloading the whole index — and a consumer can resolve it, install it, and execute code against it in a curated child image, over a local directory, a git checkout or a `file://`/HTTP base. Every step is driven through the shipped `bin/parley` binary and proven that way by the test suite. What is *not* here yet: **registry hosting**, **prerelease versions**, **index signing**, and a `parley retire` verb (retirement *records* are honoured — a retired release is excluded from fresh resolution and reported by `parley check` — but the index owner writes the record by hand).

## Installing

Parley runs from a checkout. Clone it, then put the wrapper on your `PATH`:

```bash
git clone https://github.com/leocamello/parley.git ~/src/parley
ln -s ~/src/parley/bin/parley ~/.local/bin/parley
parley --version
```

The symlink is the supported route: `bin/parley` resolves its own symlink chain — including a link to a link — before locating the checkout, so it works from anywhere on your `PATH`. If you would rather point it explicitly, set `PARLEY_ROOT` to the checkout and the wrapper honours it:

```bash
PARLEY_ROOT=~/src/parley ~/src/parley/bin/parley --version
```

An installation whose `bin/parley-main.st` is missing exits `70` naming the path it looked for — a broken install is a defect in Parley, and it will not pretend to have succeeded.

### Recording your index once, in `parley.config.st`

`--source`, `--git` and `--index` are how you tell Parley where your index is. You do not have to retype them on every command: write them once, in a `parley.config.st` at the **project root**, beside `Package.st` and `parley.lock`.

```smalltalk
#(#'parley-config' 1 #source '../my-index' #git '' #index '')
```

It is Parley's own literal format — the same one index entries and lockfiles use — read by the same reader. All three keys are present, in that order, and the empty string means *not set*. Declare **at most one**: a file naming two sources is refused, for the same reason `--source` with `--git` is a usage error rather than a silent preference.

```sh
parley resolve                  # uses ../my-index, from the file
parley resolve --source ../other  # uses ../other; the file is not read at all
```

**A flag on the command line always wins**, and when you pass one the configuration file is not consulted at all — not for the flag you gave, and not for the ones you left out. That is what makes the file safe to add to a project that already works: it can only supply a default you did not type.

It lives at the project root and **not** under `.parley/`, deliberately. That directory is regenerable state — the store, the caches, the package target — and deleting it is a fair way to clean a project. Your configuration is yours, and it survives that:

```sh
rm -rf .parley && parley resolve   # still finds your index
```

Four things can be wrong with the file, and each answers one line naming it at exit `1`, never a backtrace: it is not a regular readable file, it does not parse, it is not a `#'parley-config' 1` artifact, or its body is malformed — a missing, misordered or unknown key, a value that is not a string, or two sources at once.

```
$ parley resolve
/home/you/project/parley.config.st: malformed configuration, the keys must be #source #git #index in that order - fix it or remove it and try again
$ echo $?
1
```

No Parley command writes this file, which is why every one of those lines ends the same way: fix it or remove it. Removing it always works — the tool goes back to the flags it always had.

### Supplying the index from the environment

The same three values travel as environment variables — `PARLEY_SOURCE`, `PARLEY_GIT` and `PARLEY_INDEX` — for the caller that can neither type a flag nor write a file into your checkout: CI. Each supplies exactly what its flag would, and the precedence chain is **flag > environment > file**: a flag on the command line skips both lower levels, and a set variable skips the file, malformations included — the same skip the flag has always performed.

```sh
PARLEY_SOURCE=/ci/index parley resolve                    # resolves from /ci/index
PARLEY_SOURCE=/ci/index parley resolve --source ../other  # the flag wins; the variable is not consulted
```

An **empty variable is unset**: `PARLEY_SOURCE= parley resolve` reaches your `parley.config.st` exactly as if nothing were exported. And two set variables under a flagless command are refused, not ranked — the file's own two-source rule, applied to the environment:

```
$ PARLEY_SOURCE=/ci/index PARLEY_GIT=/ci/repo parley resolve
PARLEY_SOURCE and PARLEY_GIT are both set - the environment supplies at most one source, unset one or give a source flag
$ echo $?
1
```

## The sixty-second walk

The fastest true thing Parley can show you is the workflow that usually hurts: a library and its consumer, side by side, edited together. Sixty seconds, no index and no publish anywhere.

Make the pair, and give the library one class and a manifest (`parley init` writes a starter to edit):

```smalltalk
"demo/kernel-text/Text.st"
Object subclass: KernelText [
    KernelText class >> shout: aString [ ^aString, '!' ]
]
```

```smalltalk
"demo/kernel-text/Package.st"
Parley define: [:pkg |
    pkg
        name: 'kernel-text';
        version: '0.1.0';
        fileIns: #('Text.st') ]
```

The consumer declares the library **by path** — a directory, not a constraint:

```smalltalk
"demo/my-app/Package.st"
Parley define: [:pkg |
    pkg
        name: 'my-app';
        version: '0.1.0';
        fileIns: #('App.st');
        dependency: 'kernel-text' path: '../kernel-text' ]
```

```sh
cd demo/my-app
parley resolve                # no index needed: the dependency is a directory
cat > run.st <<'ST'
Transcript showCr: (KernelText shout: 'sixty seconds').
ObjectMemory quit: 0
ST
parley exec run.st            # → sixty seconds!
```

Now edit the sibling — make `shout:` append `'!!'` in `../kernel-text/Text.st` — and run `parley exec run.st` again. The change is there. No publish, no rebuild, no re-registration: `exec` reads a path dependency fresh on every run, and `parley.lock` records it as provenance (`#path '../kernel-text'`) rather than as a digest. When the library is ready for the world, `parley publish` takes over — and refuses a manifest that still declares a path, because a release cannot depend on a directory on your laptop.

## Commands

```
parley init                     Create a new package with a Package.st manifest
parley resolve                  Resolve dependencies and write the lockfile
parley install                  Resolve, fetch, and register dependencies
parley update [<pkg>]           Re-resolve ignoring the lockfile — or move one package
parley add <pkg> <constraint>   Declare a dependency and move the project onto it
parley remove <pkg>             Delete a dependency and move the project off it
parley search <term>            Find packages the index publishes by name
parley info <pkg>               Show a package's versions, metadata and dependencies
parley outdated                 Show which pins have newer releases, and which need a wider constraint
parley publish <dir>            Build the archive and land a release in an index
parley exec <script>            Run a program inside a curated, resolved environment
parley why <pkg>                Explain what put a package in the lockfile
parley tree                     Show the pinned dependency graph
parley check                    Verify the lockfile against the manifest and the index
parley --version                Print the version, and nothing else
parley --help                   Print these usage lines, and nothing else
```

Asking the tool a question is not an error. `parley --help`, `parley -h` and `parley help` are the same question and all three exit `0`, as `parley --version` does — and none of them creates project state, because a `.parley/` that appeared when you asked what a command does would be state created by a question. Failing to *say* what you want still is an error: `parley` with no arguments, an unknown verb, and a trailing `parley resolve --help` all exit `2`.

Three flags select the index for the verbs that need one, and they are mutually exclusive — giving two is a usage error rather than a silent preference, because a typo should not look like a working command:

```
--source <dir>    index entries in a local directory
--git <repo>      index entries in a git repository (cloned once, pulled --ff-only)
--index <base>    index entries fetched per package from a sparse index
```

Two further flags take no value and assert something about the whole invocation rather than selecting anything:

```
--offline         this invocation spawns no git and no curl
--locked          this invocation does not change parley.lock
```

`--offline` is enforced where the source is wired: a `--git` or `--index` source — given on the command line *or* recorded in `parley.config.st` — is refused at exit `1` with one line naming the tran
← Back to results