Skip to content

Package Manager

tulpar pkg is the built-in package manager for Tulpar. It manages project dependencies via a small TOML manifest (tulpar.toml) and writes a deterministic lockfile (tulpar.lock) on every install so your tulpar_modules/ tree is reproducible across machines.

The official package registry lives at pkg.tulparlang.dev — point [registry] url there in your manifest to install community packages.

Terminal window
# Inside an empty project directory
tulpar pkg init my-api
# Add a local-path dependency (development)
tulpar pkg add greeter@path:../greeter
# Add a single-file URL dependency
tulpar pkg add lodash@url:http://my-cdn/lodash.tpr
# Add a semver-style registry dependency (requires [registry] url in tulpar.toml)
tulpar pkg add wings@1.2.3
# Vendor everything into ./tulpar_modules
tulpar pkg install

After tulpar pkg install, the consumer can import "greeter" and the runtime resolves it from tulpar_modules/greeter/greeter.tpr.

tulpar.toml is a deliberately tiny TOML subset — string values only, top-level keys plus [registry] and [dependencies] tables.

name = "my-api"
version = "0.1.0"
description = "Tulpar HTTP API example"
author = "Hamza"
license = "MIT"
[registry]
url = "https://api.pkg.tulparlang.dev" # the API base; this is also the default
[dependencies]
wings = "1.2.3" # registry
greeter = "path:../greeter" # local sibling dir
lodash = "url:http://cdn/lodash.tpr" # single-file URL
FormMeaning
path:./local/dirRecursively copy *.tpr from a local directory.
url:http://example.com/x.tprPlain HTTP fetch of a single .tpr file.
1.2.3Exact version from the registry.
^1.2.3 / ~1.2.3Semver range — caret (within major) / tilde (within minor).
>=1.0,<2.0 / * / latestComparator/compound range, or highest published version.

For an exact version Tulpar hits the registry directly. For a range (^, ~, a </>/= comparator, a comma-compound, or */latest) it first fetches the package’s published versions from <registry>/v1/packages/<name>, picks the highest one that satisfies the range, then downloads <registry>/v1/packages/<name>/versions/<version>/source.

A registry version spec requires a registry URL — [registry] url = "..." in the manifest, the --registry flag, or the TULPAR_REGISTRY env var (default https://api.pkg.tulparlang.dev). https:// URLs require Tulpar to be built with OpenSSL (pacman -S mingw-w64-x86_64-openssl on MSYS2).

After every successful pkg install, Tulpar writes tulpar.lock next to your manifest:

# tulpar.lock — auto-generated by `tulpar pkg install`.
# DO NOT EDIT. Commit alongside tulpar.toml so re-installs are reproducible.
[resolved]
wings = "https://api.pkg.tulparlang.dev/v1/packages/wings/versions/1.2.3/source"
greeter = "path:../greeter"
lodash = "url:http://cdn/lodash.tpr"

The lockfile records the fully resolved URL or path plus a SHA-256 of the downloaded bytes for each dependency, so a re-install on another machine fetches the exact same bytes — even if the registry’s latest pointer moves. A re-install that finds the same URL serving a different SHA-256 than the lockfile records is refused rather than silently overwritten.

When the AOT compiler sees import "name", it tries (in order):

  1. The embedded standard library (wings, router, http_client, orm, test, …).
  2. A bundle-local sibling — <importing-file's-dir>/name.tpr — so a multi-file package’s internal imports resolve to its own files.
  3. The literal path ./name.
  4. The literal path with .tpr appended (./name.tpr).
  5. ./tulpar_modules/<name>/<name>.tpr — the vendored entry-point convention.
  6. ./tulpar_modules/<name>.tpr — single-file vendor.

That ordering means the embedded stdlib names (wings, orm, …) always win — you can’t shadow wings with a local wings.tpr — and a vendored package in tulpar_modules/ is the fallback for everything else.

CommandEffect
tulpar pkg init [name]Create a starter tulpar.toml (refuses to overwrite).
tulpar pkg listPrint package metadata + dependencies.
tulpar pkg add <name>[@<ver-spec>]Add or update a manifest dependency line.
tulpar pkg remove <name>Drop a manifest dependency line.
tulpar pkg installVendor every dep into tulpar_modules/ + write tulpar.lock.
tulpar pkg search <query>Search the registry catalog by name/description.
tulpar pkg info <name>Print a package’s registry metadata + published versions.
tulpar pkg publish [--dry-run]Bundle the project’s sources and POST them to the registry.

Packages on pkg.tulparlang.dev are published from public GitHub repositories — the registry fetches and caches your source at a ref you pin, so what installs is immutable and independent of GitHub’s uptime. There are two ways to publish:

  • From a GitHub repo (recommended, self-serve) — for anyone with an approved account. You point the registry at your public repo + ref and it fetches the source.
  • Bundle upload via the CLItulpar pkg publish, owner/maintainer-level (bearer token). Uploads your project’s .tpr sources directly.

Either way, every new package version is reviewed by the registry owner before it appears in the public catalog.

Go to pkg.tulparlang.dev and either:

  • Sign in with GitHub (recommended) — links your GitHub identity, which is required to publish a repo; or
  • Register with email + password.

New accounts start in a pending state.

The registry owner reviews new accounts and approves or rejects them. You can sign in and see your status on your account page, but you can’t publish until you’re approved — this keeps the catalog curated.

Your package lives in a public GitHub repo you own. At the ref you publish (a tag, branch, or commit), the repo must contain the package’s entry file:

  • By default, <name>.tpr at the repo root (e.g. wings_jwt.tpr), or
  • any path you pass as entry (e.g. src/mypkg.tpr).

Tag a release so the ref is stable:

Terminal window
git tag v1.0.0 && git push origin v1.0.0

Sign in, then open pkg.tulparlang.dev/publish and fill in:

FieldExampleNotes
Package namemy_pkgThe name users tulpar pkg add.
Version1.0.0Semver; immutable once published.
GitHub repoyou/my_pkgowner/name; must be under your GitHub login.
Refv1.0.0Tag, branch, or commit SHA.
Entry filemy_pkg.tprOptional; defaults to <name>.tpr.
DescriptionOne-line summaryOptional.

On submit, the registry fetches https://raw.githubusercontent.com/<repo>/<ref>/<entry>, caches the bytes, and records the version as pending owner approval.

The web form posts to the registry’s HTTP API; you can call it directly with your session token (or the owner can use the publish token):

POST https://api.pkg.tulparlang.dev/v1/publish/github
Authorization: Bearer <session-or-publish-token>
Content-Type: application/json
{
"name": "my_pkg",
"version": "1.0.0",
"repo": "you/my_pkg",
"ref": "v1.0.0",
"entry": "my_pkg.tpr",
"description": "One-line summary"
}

entry and description are optional. Ownership: a non-owner account may only publish repos whose owner matches its linked GitHub login (case-insensitive); the owner (or a holder of the registry’s publish token) may publish any repo.

A freshly published version is hidden from the public catalog (and 404s on direct fetch) until the owner approves it. Once approved it shows up in tulpar pkg search and installs with tulpar pkg add. Versions are immutable — a published (name, version) is pinned forever; re-publishing the same pair returns 409 Conflict.

The owner/maintainer path bundles every .tpr in your project and uploads the bytes directly — no GitHub repo needed. Auth is a bearer token (--token <tok> or TULPAR_PUBLISH_TOKEN):

Terminal window
export TULPAR_PUBLISH_TOKEN=""
tulpar pkg publish --dry-run # bundle + show what would ship, no upload
tulpar pkg publish # POST to <registry>/v1/publish

The registry target follows the same resolution as installs (--registry flag → [registry] urlTULPAR_REGISTRY → the default https://api.pkg.tulparlang.dev). --dry-run skips the POST so you can inspect the bundle without a token.

StatusMeaning
401 authentication requiredNot signed in (and no publish token).
403 account not approvedYour account is still pending or was rejected.
403 github_requiredPublishing a repo needs a linked GitHub login — sign in with GitHub.
403 not_your_repoThe repo isn’t under your GitHub login.
409 conflict(name, version) already published — versions are immutable.
502 fetch_failedThe registry couldn’t fetch the source — check the repo/ref/entry are public and correct.
  • Private-repo publishing (token-scoped fetch).
  • Multi-file packages from a repo (tarball extract).
  • Package signing (the lockfile already records a SHA-256 per dependency).