0k-pkg is a shell-based package management and release automation
toolkit. It provides a unified interface for building, testing, and
publishing packages across multiple ecosystems (Rust/Cargo,
Python/Hatch, TypeScript, JavaScript/npm, and shell packages).
The toolkit auto-detects the package type from project files and routes commands to the appropriate handler, enabling consistent workflows regardless of the underlying technology.
- Automatic package type detection based on project files
- Unified command interface across different package ecosystems
- Safe testing using unionfs-fuse overlays (source remains unmodified)
- Multiple source selection modes: committed, workdir, staged, intermediate commits
- Caching of test results and build artifacts
- Coverage report aggregation
- Integration with
dovisfor cross-platform testing - Extensible architecture for adding new package types
| Type | Detection | Build / publish | Extra checks and scripts |
|---|---|---|---|
| all | always | (drives the pipeline) | license, debugger, merge markers, YYY, vendored scripts; git hooks |
| autogen | autogen.sh | - | package-name check; 90-prepare.sh |
| cargo | Cargo.toml | crate → crates.io | docs.rs check; clippy/fmt/doc/org lint; 20-org-to-md.sh |
| hatch | pyproject.toml with hatchling + hatch-vcs | wheel, sdist → PyPI | pdb/rst checks; 25-changelog.sh |
| python | setup.py | wheel, sdist → PyPI | pdb/rst checks; flake8 lint; 25-changelog.sh |
| npm | package.json | npm pack → npm | transpile check |
| ts | tsconfig.json | npm pack → npm | transpile check |
| kal-sh | pkg/ + test/test | deb | - |
| charm | metadata.yml | - | - |
The pkg, lint, vendor and pkg-git-hook verbs are implemented
once, by all; the per-type directories only contribute hook
scripts. t (tests) is implemented per type.
Ensure dependencies are installed:
# Core dependency
apt install unionfs-fuse
# For YAML config parsing (Rust implementation, not the legacy PyPI one)
cargo install shyaml-rs
# Per-language dependencies
cargo install toml-cli # For Cargo/Hatch packages
apt install jq # For npm/Hatch packagesInstall 0k-pkg itself. Nothing is copied: the clone is the
installation, and everything else is a symlink into it, so a
git pull is an upgrade.
# bash-shlib must be available (provides /etc/shlib)
# dovis must be installed for testing
git clone https://github.com/0k/0k-pkg.git ~/dev/sh/0k-pkg # or wherever you keep sourcesPoint pkgcmd at the plugin tree. pkgcmd loads its package types
from \~/.pkgcmd.d/; without this symlink no command is found:
ln -s ~/dev/sh/0k-pkg/src/etc/pkgcmd.d ~/.pkgcmd.dExpose the commands on your PATH. pkgcmd dispatches on the
name it was invoked by (${0##*/}), so each verb is a symlink
named after it, placed in a directory that is already on PATH:
ln -s ~/dev/sh/0k-pkg/src/bin/pkgcmd ~/.local/bin/pkgcmd
for verb in pkg t lint pkg-git-hook; do
ln -s pkgcmd ~/.local/bin/$verb
done(vendor is reached as pkg vendor.)
The primary release workflow command. Runs sanity checks, tests, builds distribution files, validates them, and optionally publishes.
# Run full release workflow on committed code (default)
pkg
# Run on working directory state
pkg -w
# Run on staged changes only
pkg --staged
# Test all intermediate commits since last push
pkg -i
# Publish after successful validation
pkg --publish
# Skip tests (for quick iteration)
pkg --no-tests
# Skip distribution building
pkg --no-distA fresh git clone of a project must be buildable without 0k-pkg
installed: only git, sed, date and grep should be needed to
run ./autogen.sh and then the language’s own build tool. The
autotools model is followed: 0k-pkg plays the role of autoreconf
(private, developer-side), and autogen.sh plus
.package.d/autogen.d/* play the role of configure (public,
committed, self-sufficient).
pkg vendor # copy canonical autogen.sh + plugin autogen.d/ scripts
pkg vendor --dry-run # report what would changeWhat gets copied depends on the package types that apply to the
project (see each plugin’s autogen.d/ directory):
| Plugin | Script | Purpose |
|---|---|---|
autogen | 90-prepare.sh | %%version%% substitution in $FILES |
cargo | 20-org-to-md.sh | README.org → README.md (pandoc) + changelog appended, since crates.io only renders the README |
hatch | 25-changelog.sh | CHANGELOG.rst from git history (gitchangelog) |
python | 25-changelog.sh | idem |
Provenance is written to .package.d/autogen.d/MANIFEST (source
commit and a sha256 per file). Commit the vendored files together
with the manifest.
To keep a local modification of a vendored script, add the line
## pkgcmd: local-override within its first five lines; pkg vendor
then leaves it alone. Scripts that no longer exist upstream are
removed on the next pkg vendor unless modified or overridden.
At release time all/source/check/vendored verifies that every
vendored file matches its manifest entry (error on drift without
the override marker, error on a project script silently shadowing a
plugin one), and warns when 0k-pkg now ships a newer copy: run
pkg vendor, re-test, commit.
Optional tools used by vendored scripts (pandoc, gitchangelog)
are checked with depends_soft: missing, the step is skipped with a
warning so that ./autogen.sh still succeeds for someone who only
wants to build. pkg exports AUTOGEN_STRICT=1 for the whole
release pipeline, which turns those into hard errors.
Runs the test suite for the detected package type.
# Run all tests
t
# Pass arguments to the test runner
t -v # verbose (if supported by test runner)Runs linting checks appropriate for the package type.
lintRuns the git hook check scripts for a given hook name. This is the
entry point used by the global core.hooksPath trampolines (see the
git charm in 0k-cfg): each trampoline is a two-line script that
exec~s ~pkg-git-hook <HOOK> "$@".
# Called by git through ~/.config/git/hooks/pre-commit
pkg-git-hook pre-commit
# Called by git through ~/.config/git/hooks/commit-msg
pkg-git-hook commit-msg .git/COMMIT_EDITMSGFor a hook HOOK, every executable in git-hook/HOOK/ of each
applying package type is run in sorted order with the arguments git
passed to the hook. The first non-zero exit aborts and becomes the
hook’s exit code. On success the repository-local
.git/hooks/HOOK is exec‘d if present, so per-repository hooks
keep working underneath the global ones.
The command is silent on success, as git hooks are expected to be.
It is not named git-hook: git prepends its --exec-path to PATH
when running hooks, and git-core/git-hook (the git hook
builtin) would shadow it.
Checks shipped for the all package type:
| Hook | Script | Purpose |
|---|---|---|
pre-commit | 10-readme-md | Block README.md when README.org exists |
pre-commit | 20-opencode-paths | OpenCode sessions: block doc/admin.org, .sisyphus/, *.md |
pre-commit | 30-org-clock | org-cli clock check on staged *.org |
commit-msg | 10-opencode-msg | OpenCode sessions: block Co-authored-by / “Ultraworked” |
commit-msg | 20-convention | Commit message convention audit |
commit-msg | 90-assisted-by | Add Assisted-by: trailer from ai-audit |
The *-opencode-* hooks only act when the $OPENCODE environment
variable is 1 (set by the OpenCode agent harness) and are inert
otherwise. 30-org-clock needs org-cli and 90-assisted-by
needs ai-audit; both hooks skip silently when the tool is absent.
The pkg command supports multiple source selection modes:
| Option | Description |
|---|---|
-c REV | Use specific commit (default: HEAD) |
-w | Use current working directory state |
--staged | Use only staged changes |
-i | Test each commit since last push |
--since REV | Test each commit since specified revision |
| Option | Description |
|---|---|
--no-tests | Skip test execution |
--no-dist | Skip distribution file creation |
--no-dist-check | Skip distribution file validation |
--no-sanity-check | Skip pre-release sanity checks |
--no-cover | Disable coverage collection |
--no-cache | Prevent caching of results |
--publish | Publish to package registry after checks |
-s | Open interactive shell in test environment |
Global configuration is stored in:
\~/.config/pkg/main.yml- Main configuration file (YAML format)\~/.pkgcmd.d/- Package types and their hook scripts. Normally a symlink to the clone’ssrc/etc/pkgcmd.d(see Installation); a real directory holding your own<type>/cmd.shworks too.
Projects can provide local overrides in:
.pkgcmd.d/- Project-specific command handlers.package- Legacy package configuration (shell script, sourced)
Package types are not exclusive: several apply to one project at
the same time and their contributions stack. On a Rust project
with an autogen.sh, all, autogen and cargo all apply.
When a command is invoked, pkgcmd walks up the directory tree
until at least one package type’s applies function returns true,
then:
- Verb dispatch. Among the applying types that implement
pkgcmd:<verb>:<type>:run, the first one (alphabetical order of the function table) runs the verb.allimplementspkg,lint,pkg-git-hookandvendor;cargoimplementst; and so on. In practice each verb has one implementation and the choice is unambiguous. - Hook scripts. The verb implementation exports
$PKGCMD_ALTERNATIVES, the list of every applying type, andpkg._list_scripts <hook>unions the project’s.package.d/<hook>/*with<type>/<hook>/*for each of those types. So the release checks, lint scripts, git hooks andautogen.d/scripts fromall,autogenandcargoall run, sorted by file name.allis therefore not a fallback but the layer shared by every project.
Handlers are loaded from \~/.pkgcmd.d/*.sh and
\~/.pkgcmd.d/*/cmd.sh, then from any .pkgcmd.d/ found in the
current directory or its parents.
- Source Preparation: Clone/copy source to temporary directory based on selected source mode
- Sanity Checks: Run scripts from
source/check/(license, debugger statements, merge markers, vendored scripts in sync) - Tests: Execute via
dovisacross configured platforms - Coverage: Aggregate coverage reports if enabled
- Dist Checks: Run scripts from
dist/check/ - Build: Execute scripts from
release/build/(wheel, sdist, npm pack, etc.) - Release Checks: Validate built artifacts via
release/check/ - Publish: If
--publish, execute scripts fromrelease/publish/
pkg uses unionfs-fuse to create copy-on-write overlays. Tests run
in an isolated environment where:
- The original source remains untouched
- Modifications are written to a temporary upper layer
- Each test run starts from a clean state
Build artifacts are placed in /tmp by default.
bin/test # runs the sunit suite in test/
bin/test vendor # one filebash-shlib- Shell library frameworkunionfs-fuse- Filesystem overlay supportdovis- Cross-platform test runnershyaml- YAML query tool, from theshyaml-rscrate (cargo install shyaml-rs)
| Language | Dependencies |
|---|---|
| Cargo/Rust | toml-cli, pandoc, gitchangelog (release only) |
| Hatch | toml-cli, jq, hatch, docutils (rst check), gitchangelog |
| Python | docutils (rst check), flake8 (lint), gitchangelog, nosetests (if configured) |
| npm/TS | jq, npm |
To add support for a new package type, create a handler in
\~/.pkgcmd.d/<type>/cmd.sh or \~/.pkgcmd.d/<type>.sh:
# Detection function - return 0 if this type applies
pkgcmd:<type>:applies() {
[ -e "marker-file" ]
}
# Optional: mark as base type for inheritance
pkgcmd:<type>:is_base() {
[ -e "marker-file" ]
}
# Command implementations
pkgcmd:t:<type>:run() {
# Run tests
your-test-command "$@"
}
pkgcmd:pkg-name:<type>:run() {
# Output package name
echo "my-package"
}
pkgcmd:pkg-version:<type>:run() {
# Output package version
echo "1.0.0"
}For release workflow integration, create executable scripts in:
<type>/source/check/- Pre-test sanity checks<type>/dist/check/- Distribution validation<type>/release/build/- Build distribution files<type>/release/check/- Post-build validation<type>/release/publish/- Publishing commands<type>/git-hook/<hook>/- Git hook checks run bypkg-git-hook<type>/autogen.d/- Scripts copied into projects bypkg vendorand sourced by./autogen.sh(not executed: no shebang, not executable;return 1aborts the step). Usedepends_softfor optional tools.
See LICENSE file for details.