Hive - the Bee package manager
Hive is to Bee what pip is to Python: one command that fetches a package, puts it somewhere the interpreter looks, and remembers what you installed.
hive install strutil # from the registry
hive install strutil@1.2.0 # a specific version
hive install ./strutil-1.2.0.pkg # from a local package file
hive install # everything in hive.json
After that, bee just finds it:
Hive ships alongside bee - the .deb and the Windows installer both put
hive on your PATH. Building from source produces both binaries:
Where packages go
A project keeps its packages next to its code, in hive_modules/:
my-app/
├── hive.json what this project depends on
├── hive.lock exact versions + hashes that were installed
├── main.bee
└── hive_modules/
├── .hive/ hive's own install records - don't edit
│ ├── greet.json
│ └── logger.json
├── greet/
│ ├── hive.json
│ └── init.bee
└── logger/
├── hive.json
└── init.bee
hive install -g installs into a shared library instead - ~/.hive/lib
(%USERPROFILE%\.hive\lib on Windows), which every script on the machine can
import. Use it for tools you want everywhere, and the project-local default for
anything a project actually depends on.
How bee finds a module. import name searches, in order:
- the importing file's own directory
- its sibling
lib/directory hive_modules/in that directory and in every directory above it- each entry of
$BEE_PATH(;-separated on Windows,:elsewhere) - the global library,
$HIVE_HOME/lib(default~/.hive/lib)
Local code wins over an installed package of the same name, so a project can
always override something it installed. Searching hive_modules/ all the way up
the tree is what lets an installed package import its dependencies from the
one flat directory, with no nesting and no duplicate copies.
At each root, import greet tries greet.bee, greet.be, greet, and then
greet/ as a package directory - whose entry module is the "main" from its
hive.json, falling back to init.bee, init.be, greet.bee, main.bee.
import pkg.submodule maps to pkg/submodule.bee, so a package can expose more
than one module.
Commands
| Command | What it does |
|---|---|
hive install |
install every dependency in hive.json |
hive install <name> |
install the newest version that fits, and save it to hive.json |
hive install <name>@<constraint> |
install a specific version or range |
hive install <file.pkg> |
install a local package file; its dependencies still come from the registry |
hive uninstall <name>... |
delete the package and drop it from hive.json |
hive list |
show what's installed, with versions |
hive info <name> |
a package's versions, dependencies and description |
hive search <query> |
search the registry index |
hive init [dir] |
write a starter hive.json (and an init.bee if there's none) |
hive pack [dir] |
build a .pkg package from a package directory |
hive cache dir / hive cache clean |
show or clear the download cache |
add/i, remove/rm, ls, show and build work as aliases.
Options
| Option | Effect |
|---|---|
-g, --global |
act on ~/.hive/lib instead of ./hive_modules |
-u, --update |
ignore hive.lock pins and take the newest match |
--offline |
never touch the network; install from the lockfile and cache |
--registry <url> |
use this registry for one command |
--no-save |
install without recording it in hive.json |
--force |
overwrite a directory Hive didn't create |
-o, --output <file> |
where hive pack writes the archive |
-C, --dir <dir> |
treat this directory as the project root |
-q, --quiet |
print only errors |
Commands work from anywhere inside a project: Hive walks up to the nearest
directory with a hive.json, the way git finds its repository.
hive.json
The manifest describes a package - or, in an application, just what it needs.
{
"name": "strutil",
"version": "1.2.0",
"description": "String helpers for Bee",
"author": "Atanu Debnath",
"license": "MIT",
"homepage": "https://github.com/you/strutil",
"repository": "https://github.com/you/strutil",
"keywords": ["strings", "text"],
"main": "init.bee",
"dependencies": {
"logger": "^1.0.0"
},
"files": ["init.bee", "src"],
"exclude": ["tests", "notes.md"]
}
| Field | Meaning |
|---|---|
name |
lowercase letters, digits, - and _, starting with a letter. It becomes a directory name and what you type in import, so it has to work as both. Required to publish. |
version |
MAJOR.MINOR.PATCH, optionally -prerelease. Required to publish. |
main |
the module import <name> loads. Defaults to init.bee. |
dependencies |
package name → version constraint |
files |
optional whitelist for hive pack; a directory name includes everything under it. hive.json is always included. |
exclude |
optional prune list for hive pack |
build |
a command run once in the package directory right after install |
An application's hive.json can be little more than {"dependencies": {...}} -
name and version are only required for something you intend to publish.
Unknown fields are preserved when Hive rewrites the file, so you can keep your own metadata in there.
hive.lock
hive install writes hive.lock with the exact version, URL and SHA-256 of
everything it resolved:
{
"lockVersion": 1,
"packages": {
"greet": {
"version": "1.2.0",
"url": "https://packages.beelang.dev/files/greet-1.2.0.pkg",
"sha256": "bbfaecb3…",
"dependencies": { "logger": "^1.0.0" }
}
}
}
Commit it. A later hive install reuses those pins whenever they still satisfy
your constraints, so everyone on the project - and CI - installs the same bytes.
Because the lockfile carries the URL and hash, hive install --offline needs no
registry at all: it installs straight from the cache. hive install -u ignores
the pins and moves to the newest matching versions.
Version constraints
| Constraint | Matches |
|---|---|
1.2.3 or =1.2.3 |
exactly that version |
^1.2.3 |
>=1.2.3 without changing the leftmost non-zero part - 1.9.0 yes, 2.0.0 no |
~1.2.3 |
patch updates only - 1.2.9 yes, 1.3.0 no |
>=1.2.3, >1.2.3, <=2.0.0, <2.0.0 |
the obvious thing |
*, latest, or omitted |
any version |
>=1.2.0, <2.0.0 |
comma- or space-separated terms must all hold |
hive install greet records ^<installed version>, which is the usual "keep up
with compatible releases" default. A prerelease sorts before its release, so
1.0.0-rc1 < 1.0.0.
Hive picks the highest non-yanked version satisfying every constraint on a package. When nothing satisfies them all, it says which constraint came from where instead of guessing:
Writing and publishing a package
mkdir strutil && cd strutil
hive init # writes hive.json and a starter init.bee
$EDITOR init.bee
hive pack # -> strutil-1.2.0.pkg, and prints its sha256
init.bee is the package's public surface. Everything it defines is what
import strutil exposes; names starting with _ stay private to the package
(from strutil import * skips them).
Test it against a real project before publishing:
To publish, upload the .pkg file somewhere your registry can serve it and add
a version entry with its sha256 (which hive pack prints) to the registry
metadata - see below.
The .pkg package format
A .pkg file is a self-contained container: no zip, no tar, no third-party
library on either side.
Decompressed, the payload is:
<header-byte-length>\n
{"format":1,"manifest":{…},"files":[{"path":"init.bee","size":188,"sha256":"…"}]}\n
<the file bytes, concatenated in `files` order>
The payload is compressed with a small built-in LZSS - written out rather than pulled in, because a package manager that needs zlib to read its own format has a dependency problem. It roughly halves a package, and makes the file a binary blob rather than a text file with the sources sitting in it.
The compressed bytes are then XORed with a keystream. This is obfuscation, not
encryption, and the difference matters: the key is a constant in
src/hive/archive.cpp. It stops a package from being browsed or hand-edited in
a text editor. It does not keep anything in a package secret - anything that can
install a package can also extract one - so nothing belongs in a package that
needs to stay private.
Integrity is the layer that does carry weight. Every file carries its own SHA-256, and the reader rejects the package if a hash doesn't match, if bytes are missing, if there are extra bytes at the end, or if any path would escape the package directory - a corrupt or tampered download fails loudly instead of installing half a package.
Packages that have to be built
A package containing a native module can only ship one platform's binary. Rather than leaving every user to notice that and compile it by hand, a package declares how to build itself:
hive install runs that command once, in the installed package's directory,
right after unpacking and after its dependencies are in place:
The command is printed before it runs, because it came from a downloaded
package and running it silently would be worse. hive install --no-build skips
every build step.
If a build fails, its output is shown and hive exits non-zero, but the files
stay in place - a build usually fails for a fixable reason like a missing
compiler or -dev package, and re-running the command by hand in the package
directory is then the whole fix.
Two things to know when writing one:
- Invoke an interpreter explicitly -
bash build.sh, not./build.sh. A.pkgdoes not carry the executable bit, so the script will not be runnable on its own after unpacking. - Keep it portable, or fail clearly. The command runs through the system shell on whatever platform the user is on.
Running a registry
A registry is static files. GitHub Pages, S3, any web server, or a directory on disk all work; there's nothing to run.
<registry>/
├── index.json the catalogue `hive search` reads
├── packages/
│ ├── strutil.json per-package metadata `hive install` reads
│ └── logger.json
└── files/
├── strutil-1.2.0.pkg
└── logger-1.0.0.pkg
packages/<name>.json:
{
"name": "strutil",
"description": "String helpers for Bee",
"homepage": "https://github.com/you/strutil",
"versions": {
"1.1.0": {
"url": "files/strutil-1.1.0.pkg",
"sha256": "9f2c…",
"dependencies": {},
"yanked": true
},
"1.2.0": {
"url": "files/strutil-1.2.0.pkg",
"sha256": "bbfa…",
"dependencies": { "logger": "^1.0.0" }
}
}
}
url may be absolute or relative to the registry root - relative keeps a mirror
working when you copy it elsewhere. yanked: true hides a version from new
resolutions without breaking a lockfile that already names it.
index.json, used only by hive search:
{"packages": [
{"name": "strutil", "version": "1.2.0", "description": "String helpers for Bee"},
{"name": "logger", "version": "1.0.0", "description": "Tiny logging helpers"}
]}
Point Hive at your own with --registry, HIVE_REGISTRY, or
~/.hive/config.json. A filesystem path works too, which is how the test suite
runs without a network:
Configuration
| Setting | Where |
|---|---|
| Registry | --registry, else $HIVE_REGISTRY, else ~/.hive/config.json, else https://packages.beelang.dev |
| Hive home | $HIVE_HOME, default ~/.hive - holds lib/, cache/, config.json |
Extra module roots for bee |
$BEE_PATH |
Downloads are cached in $HIVE_HOME/cache, keyed by content hash - so a cache
hit verifies itself, and two packages shipping identical bytes share one entry.
Security
Hive is careful about the parts that install untrusted bytes:
- Hashes are checked, not trusted. A download whose SHA-256 doesn't match the registry's is discarded, never unpacked. Within an archive, every file has its own hash.
- Archives can't escape their directory. Absolute paths,
.., drive letters and backslashes are rejected outright, so a crafted.pkgcan't write outsidehive_modules/<name>/. - Your files aren't collateral. Hive records what it installed and refuses
to delete a directory it didn't create unless you pass
--force. - URLs aren't shell commands. Downloads shell out to
curlorwget, and a URL containing anything outside the legal URL character set is refused rather than escaped. - Offline means offline.
--offlinenever opens a connection.
What Hive does not do yet: signed packages, and any check on what a package's code does once you import it. Installing a package runs no install scripts - but importing one runs its code, so treat a package the way you'd treat any dependency.