Smalltalk packages, resolved by conversation.
# Parley
[](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