A small, dependency-light toolkit for FreePascal / Delphi projects.
One executable, fifteen command families, driven by a single lwpt.toml
manifest. Zero-install by default — git clone && fpc @lwpt.cfg
builds a project without running lwpt install first.
lwpt init scaffold a new project or adopt an existing manifest [--adopt]
lwpt install resolve + fetch dependencies, write lwpt.lock + lwpt.cfg
lwpt add add a dependency to lwpt.toml + install it [--name <name>]
lwpt remove remove dependencies from lwpt.toml + prune their modules
lwpt outdated compare locked git-host deps to advertised tags [--json]
lwpt update bump constraints + reinstall newer git-host deps [name ...]
lwpt build compile manifest build entries [--mode dev|release] [--clean] [--jobs N]
lwpt format format uses-clauses + identifiers [--check]
lwpt duplication report manifest-scoped Pascal token clones [--json]
lwpt test discover, compile and run *.Test.pas files [--jobs N] [--bail N]
lwpt repair reclaim install, build-session, and worker-lease residue, and upgrade a v3 lockfile to v4
lwpt registry run a self-hosted registry origin or verified mirror, or publish to one <init|sync|verify|rotate-key|publish|issue-token|revoke-token|serve>
lwpt run invoke a user-declared run task (or alias a subcommand)
lwpt health report Pascal complexity and optional Git hotspots [--json] [--hotspots]
lwpt agents write/verify the agent-facing command reference in AGENTS.md [--check]
LWPT is pre-1.0. The package model, install pipeline, formatter, test runner,
duplication analysis, codebase-health report, and release flow are in place.
The project-only release architecture check remains separate from the customer
commands originally deferred by
ADR-0006. See the consumer guide for using LWPT,
AGENTS.md for contributing to LWPT, and
docs/adr/ for the architectural decisions that shape
the v1 design.
The project's durable direction and delivery gates live in
VISION.md,
DEFINITION_OF_READY.md, and
DEFINITION_OF_DONE.md.
Install the released toolkit on macOS, then scaffold in your own project:
brew install fpc frostney/tap/lwpt
mkdir my-project
cd my-project
lwpt --help
lwpt init --yes
lwpt buildContinue with Use LWPT in your project for a
complete CLI, native test, dependency manifest, run task, and quality commands.
It also covers release installation on other platforms. Each subcommand's help
links back to lwpt --help so its other capabilities remain discoverable.
Contributing to LWPT itself? Follow the contributor quick start for the source bootstrap and self-host build. The bootstrap belongs to the LWPT repository; a consumer uses the installed executable.
Documentation for agents: llms.txt links directly to the same canonical Markdown guides and references.
The package manager is the foundation. install resolves the
dependency graph and emits lwpt.cfg (an FPC response fragment of
-Fu search paths). Every other subcommand consumes that same cfg.
The manifest is the single source of truth. This through-line is
deliberate — see docs/adr/0002-lwpt-namespace-zero-install.md
for the full rationale.
| File | Origin | Role |
|---|---|---|
source/lwpt.pas |
new | program entry: registers subcommands |
source/LWPT.Core.pas |
new | project identity, error hierarchy, and shared low-level helpers |
source/LWPT.Manifest.pas |
new | manifest model, intake, source/version parsing, and manifest path context |
source/LWPT.Install.pas |
new | install transaction: resolve, fetch or restore, extract, lockfile/cfg, frozen verification |
source/LWPT.Command.*.pas |
new | command-level behavior for each subcommand |
source/LWPT.Formatter.pas |
converted from GocciaScript format.pas |
formatter engine used by LWPT.Command.Format |
source/LWPT.GitProtocol.pas |
new | git smart-HTTP tag listing for <source>@<spec> resolution |
source/Platform.pas |
LWPT-canonical | host OS / CPU detection for {platform.*} placeholders (extraction candidate for packages/platform/) |
source/Shared.inc |
LWPT-canonical | include file ({$mode delphi} {$H+} baseline; each packages/<name>/source/ has its own bundled copy) |
packages/httpclient/ |
LWPT-canonical workspace package | HTTP/1.1 + HTTPS client + byte-safety accumulator |
packages/cli/ |
LWPT-canonical workspace package | option parser + subcommand dispatch + interactive prompts |
packages/semver/ |
LWPT-canonical workspace package | full node-semver port |
packages/toml/ |
LWPT-canonical workspace package | TOML 1.1 parser |
packages/testing/ |
LWPT-canonical workspace package | TestingPascalLibrary — assertion + suite + runner framework for *.Test.pas files |
The five workspace packages live under packages/<name>/ (per
ADR-0014 +
ADR-0015);
the root manifest auto-discovers them via [workspaces] include = ["packages/*"]. Per
ADR-0017, LWPT is the
canonical source for every package — and GocciaScript (a sister project
under the same owner) is the first named consumer, committed to Path A
adoption (full toolchain migration to lwpt build / install / test / format). Phase 2 graduates individual packages to standalone repos
when warranted; the per-package roadmap lives in
docs/packages.md.
my-project/
├── lwpt.toml # manifest (single source of truth)
├── lwpt.lock # lockfile (committed)
├── lwpt.cfg # FPC response fragment (committed)
├── .lwpt/ # toolkit state
│ ├── modules/ # extracted deps — COMMITTED, source of truth
│ │ ├── horse/
│ │ └── jhonson/
│ ├── archives/ # *.tar.gz per dep — COMMITTED (verification)
│ ├── tmp/ # install workspace — GITIGNORED
│ ├── sessions/ # default private build-session staging
│ └── session-roots # relocated-root ledger — GITIGNORED
├── build/ # FPC output — GITIGNORED
└── src/
└── main.pas
[package]
name = "myapp"
version = "1.4.2"
units = ["src"]
[dependencies]
# Bare-string shorthand: "<source>@<spec>" — see ADR-0009.
horse = "HashLoad/horse@^4.0.0" # GitHub by default, SemVer range
hello = "octocat/Hello-World@1.0.0" # exact SemVer (matches tag `1.0.0` or `v1.0.0`)
ci-debug = "gitlab:gitlab-examples/ci-debug-trace@dd648b2e48ce6518303b0bb580b2ee32fadaf045" # GitLab via prefix, commit SHA
atlaskit = "bitbucket:atlassian/atlaskit@d7ac1acad54ed82e3fc244398cd29044f9bf1775" # Bitbucket via prefix, full commit SHA (must be on a branch or tag)
custom = "https://example.com/custom-1.0.0.tar.gz" # arbitrary HTTPS tarball
leaf = "../leaf" # local sibling path
# Inline-table form for advanced options (include / exclude filters,
# formatter-mirror semantics — see ADR-0009):
horse-mw = { source = "HashLoad/horse", version = "^4.0.0", include = ["src/middleware/**"] }
horse-no-tests = { source = "HashLoad/horse", version = "^4.0.0", exclude = ["tests/**", "examples/**"] }
# Custom hosts via [sources.<name>] — gitea/forgejo/self-hosted/etc.
mylib = "gitea:team/mylib@^1.0.0" # uses the [sources.gitea] entry below
json = "registry:json@^1.2.0" # signed registry record (ADR-0051); see [registries] below
[sources]
# Per-project custom prefix definitions. Each entry is an inline
# table with `archive` + `git` URL templates. Placeholders are
# {user} / {repository} / {ref}. The smart-HTTP tag listing uses
# the `git` URL; the archive download uses `archive`.
# See ADR-0009 §"Custom hosts".
gitea = { archive = "https://git.example.com/{user}/{repository}/archive/{ref}.tar.gz", git = "https://git.example.com/{user}/{repository}.git" }
[registries]
# Registry origins that `registry:` dependencies resolve through (ADR-0051).
# Root manifest only. `default` is optional with exactly one registry.
default = "corp"
[registries.corp]
identity = "https://packages.example.com" # optional; advertised by the contacts, then locked
# The trust pin: the origin's root key. EXAMPLE VALUES ONLY: a valid,
# throwaway pair whose private key was discarded. Replace both with your
# origin's root pin, its `keys/ed25519-*.toml` record's key_id and public_key
# (see docs/registry-deployment.md); key-id is "ed25519:" + the SHA-256 of
# the raw 32-byte public key.
key-id = "ed25519:77790c39520108490b51dd825c9c84dc009f92ac5d1b94d68f19c9d11cca3675"
public-key = "hex:fd49e4bc086e9b5203d162066dc459ca66478324dc3b39d4e202460c0bc3e724"
origin = "https://packages.example.com" # origin contact; defaults to identity
mirrors = ["https://mirror.example.net/lwpt"] # optional; tried first, in this order
[build]
# Single-binary shorthand: `[build] source = "..."` defaults the
# entry name to [package].name and the output to build/<entry-name>.
# Multi-binary form (used here): one inline table per entry.
cli = { source = "src/cli.pas", output = "bin/cli",
target = { os = "linux", architecture = "aarch64",
abi = "", environment = "" } }
tool = { source = "src/tool.pas", output = "bin/tool", compiler = "custom" }
delphi-tool = { source = "src/tool.dpr", output = "bin/tool.exe", compiler = "delphi-win64" }
[prebuild]
generate = { command = "tools/generate", args = ["--output", "src/Generated.inc"], inputs = ["schemas/**/*.json"], output = "src/Generated.inc" }
[deploy]
command = "tools/deploy"
args = ["--environment", "staging"]
[compiler]
# Optional root-owned policy. Without it, build and test use built-in FPC.
default = "native"
[compiler.profiles.native]
driver = "fpc"
version = "^3.2.0"
[compiler.profiles.wasm]
# Opt-in adapter for the released frostney/lakon compiler.
driver = "lakon"
command = "tools/lakon" # optional; otherwise resolved from PATH
version = ">=0.1.0"
[compiler.profiles.blaise]
# Built-in opt-in adapter for graemeg/blaise v0.13.0 or newer.
driver = "blaise"
command = "tools/blaise" # optional; otherwise resolved from PATH
version = ">=0.13.0"
[compiler.profiles.custom]
# External drivers receive versioned canonical TOML on stdin/stdout.
driver = "my-compiler"
command = "tools/my-compiler-driver"
args = ["--protocol=lwpt"]
version = "^1.0.0"
[compiler.profiles.delphi-win64]
# Built-in, opt-in consumer driver; LWPT itself remains FPC-built.
driver = "delphi"
# Replace this placeholder with the installed compiler path.
command = "C:/path/to/dcc64.exe"
version = ">=36.0.0"
[version]
output = "src/Version.Generated.inc"
prefix = "APP" # generates APP_VERSION, APP_BUILD_DATE
[lwpt]
# Toolkit-state overrides. Defaults shown; you almost never need these.
# modules-dir = ".lwpt/modules"
# archives-dir = ".lwpt/archives"
# tmp-dir = ".lwpt/tmp"
# sessions-dir = ".lwpt/sessions" # LWPT_SESSION_DIR overrides this
# cfg-file = "lwpt.cfg"
[format]
# include = additive glob list on top of [package].units;
# exclude = glob list subtracted from the resolved set.
# Plain dir names are top-level shorthand; recursion via ** is explicit.
# See ADR-0007 + docs/code-style.md for the full algorithm.
include = ["tests/**/*.pas"]
exclude = ["src/legacy/Vendored.pas"]Source kinds: skGitHost (default github, with gitlab: / bitbucket: / any user-declared [sources.<name>] prefix), skURL (any https://...), skLocal (any path or local: prefix), skWorkspace (workspace:* or workspace:^X.Y.Z, naming a member discovered through [workspaces]), and skRegistry (registry:[<alias>/]<package>, selected from the registry snapshot named by the signed checkpoint of an origin declared in the root [registries] table; see ADR-0051). Version specs go through the LWPT-canonical Semver unit (a node-semver port adapted from GocciaScript's earlier copy) for ranges + exact matches, then fall through to literal Git tag / commit-SHA lookup. Tag listing uses git smart-HTTP info/refs?service=git-upload-pack — works against any git host with one URL pattern, no JSON, no auth tokens. Custom hosts (Gitea, Forgejo, self-hosted GitHub Enterprise / GitLab / Bitbucket Server) plug in via the [sources] table — no code change needed. See ADR-0009.
For a consumer dependency manifest and a runnable example, start with the consumer guide. The workspace auto-discovery below describes LWPT’s own monorepo.
TestingPascalLibrary lives in the testing workspace package and is
auto-discovered via [workspaces] include = ["packages/*"] in the
root manifest — lwpt install publishes its validated snapshot into
.lwpt/modules/testing/, and the cfg emitter wires the -Fu / -Fi paths so
every *.Test.pas file resolves uses TestingPascalLibrary; with no
further setup.
Then a *.Test.pas file is a self-contained program:
program Math.Test;
{$mode delphi}{$H+}
uses TestingPascalLibrary;
type
TMathTests = class(TTestSuite)
public
procedure SetupTests; override;
procedure TestAddition;
end;
procedure TMathTests.TestAddition;
begin
Expect<Integer>(2 + 2).ToBe(4);
end;
procedure TMathTests.SetupTests;
begin
Test('addition works', TestAddition);
end;
begin
TestRunnerProgram.AddSuite(TMathTests.Create('Math'));
TestRunnerProgram.Run;
ExitCode := TestResultToExitCode;
end.lwpt test discovers *.Test.pas files and compiles/runs independent
programs concurrently within the shared machine worker budget. --jobs=N
sets a lower per-invocation ceiling. --bail=N stops after N compile or
runtime failures, terminates and reaps active children, and leaves later
programs unstarted; zero runs the complete queue. The project default is
configured with [test] bail = N and defaults to zero. Results are printed
in source-path order, and the command exits 1 if any test or compile fails.
Every subcommand accepts a long-only --silent option after the command name.
On success it suppresses progress and child output and prints only the final
lwpt <command>: completed in ... line on stdout. On failure it replays the
retained diagnostic and failed child output before one final failure line on
stderr. --silent cannot be combined with --verbose; aliases such as
lwpt run build --silent report the resolved build identity. Because silent
mode cannot display prompts, lwpt init --silent additionally requires
--yes or --adopt.
Per ADR-0017, LWPT is the canonical source for every workspace package; the older GocciaScript copies of these units are frozen pending Path A adoption. The places where the LWPT-canonical version meaningfully differs from GocciaScript's older copy:
packages/httpclient/source/HTTPClient.pas— byte-safeAppendRawBytesaccumulator on the header-recv path and the chunked-body seed-buffer.Copy(PAnsiChar(...))truncates response bytes at the first#0, corrupting binary downloads; the byte-safe accumulator avoids the issue.packages/cli/source/CLI.Parser.pas— valued short options support separated values (-o output, including values beginning with-) and opt-in attached values (-Fusource,-dDEBUG) with longest-prefix matching. Space-separated long values (--mode release) work for plain string/integer options, not only repeatable ones. Plus theAStartArgparameter forlwpt run <subcommand>aliasing.packages/cli/source/CLI.Options.pas—TGoccia*type-prefix stripped from every public type; GocciaScript-engine-specific option groups removed as dead code.packages/semver/source/Semver.pas— renamed fromGoccia.Semver;MAX_SAFE_INTEGERinlined.packages/toml/source/TOML.pas— renamed fromGoccia.TOML; parser refactored to a class-based AST shape.source/Platform.pas— renamed fromGoccia.Platform.
See docs/packages.md for the complete
package set + per-file divergence table + bootstrap chicken-and-egg
story.
-
Consumer guide — install LWPT and use its capabilities in your project.
-
Documentation index — topic-by-topic navigation.
-
llms.txt — compact index with direct Markdown links for agents.
-
AGENTS.md— contributor instructions for AI assistants changing LWPT itself. -
docs/adr/— architectural decision records. -
docs/spikes/— point-in-time snapshots of investigations (e.g. the archived HTTP registry spike, prior art for the self-hosted registry that shipped under issue #29). -
docs/— full set of canonical docs:architecture.md,quick-start.md,tooling.md,code-style.md,build-system.md,deployment.md,testing.md,packages.md,ci.md. Each opens with an Executive Summary.