Mint portable launcher scripts and manage Linux desktop integration from a declarative spec.
launch-scaffolder is a single Rust binary that generates, installs,
and maintains launcher scripts for any hyperpolymath project (or any
other project that adopts the hyperpolymath launcher standard). Native
provisioning currently targets Linux; macOS and Windows integration remain
planned. It turns launcher scripts into
generated artefacts, not hand-edited files.
Inputs:
-
launcher-standard_praxis.deed— a declarative description of what a compliant launcher looks like (modes, file paths, permissions, integrity requirements, per-platform behaviour), written in the estate’s DEED v1.0.0 format. The canonical copy lives in thestandardsmonorepo atstandards/launcher/launcher-standard_praxis.deed. At runtime the loader resolves in this order:-
--standard<file>CLI flag -
$LAUNCH_SCAFFOLDER_STANDARDenvironment variable -
Every rung of the search ladder the standard declares for itself, in ascending
:priorityorder — see(resolution)→standard-searchinside the deed. A rung naming an unset$VARis skipped, never expanded to an empty path. -
A baked-in copy compiled into the binary at build time (fallback only, and logged at
infoso the downgrade is visible)
-
-
<app>.launcher.a2ml— per-app config (name, display, URL, command, repo, icon, and an optional[exceptions]block).
Output:
-
<app>-launcher.sh— a fully spec-compliant cross-platform launcher (Linux / macOS / Windows-via-Git-Bash).
Without this tool, every hyperpolymath project maintains its own
hand-written ~600-line bash launcher script that has to
be kept in sync with the current launcher standard by hand. In practice,
drift is guaranteed. The launcher standard evolves; the launchers don’t.
Fixing a spec issue means editing 11+ files and hoping you caught them
all.
With this tool:
-
The standard is a single file. Changing it is a one-line edit.
-
Every launcher is regenerated from the same standard + its own config.
-
Bulk realignment after a spec change is one command:
launch-scaffolderrealign*.launcher.a2ml. -
Launchers become reproducible, auditable artefacts — not hand-edited drift.
-
“Not wasting tokens in future” — future AI sessions read one spec file, not 11 near-identical launcher scripts.
CLI subcommands, in the order you’re likely to use them:
# ─── mint ──────────────────────────────────────────────────────────────────
# Generate a launcher script from a config. Writes
# <config-parent>/<app-name>-launcher.sh by default.
launch-scaffolder mint examples/stapeln.launcher.fixture.a2ml
launch-scaffolder mint /path/to/burble.launcher.a2ml -o /tmp/out.sh
launch-scaffolder mint /path/to/burble.launcher.a2ml --stdout
# ─── realign ───────────────────────────────────────────────────────────────
# Bulk re-mint: walk the estate (default = /var/mnt/eclipse/repos) and
# re-render every live `<app>.launcher.a2ml` against the current standard
# + template. Idempotent — unchanged scripts stay unchanged.
launch-scaffolder realign # walk estate root
launch-scaffolder realign --search-root ~/my-projects # narrow walk
launch-scaffolder realign /path/to/one.launcher.a2ml # explicit configs
launch-scaffolder realign --dry-run # preview only
launch-scaffolder realign --check # CI mode: exit 1 on any diff
launch-scaffolder realign --keep-going # don't stop on errors
# ─── provision ─────────────────────────────────────────────────────────────
# Install (--integ) or uninstall (--disinteg) a launcher's desktop entry,
# icon, and launcher binary on the current system. Writes .desktop files
# directly from the Rust binary rather than going via the generated shell
# script. Bulk runs (--all) prompt before touching $HOME.
launch-scaffolder provision --integ /path/to/burble.launcher.a2ml
launch-scaffolder provision --disinteg /path/to/burble.launcher.a2ml
launch-scaffolder provision --integ --all # everything in the estate
launch-scaffolder provision --integ --all --no-confirm --force
launch-scaffolder provision --integ --all --dry-run # preview only
# ─── standard ──────────────────────────────────────────────────────────────
launch-scaffolder standard show
launch-scaffolder standard validate
# ─── config ────────────────────────────────────────────────────────────────
# Read/validate either metadata dialect. `set` can edit legacy A2ML blocks;
# current DEED blocks are read-only and should be changed at the source config.
launch-scaffolder config get ./stapeln-launcher.sh version
launch-scaffolder config get ./stapeln-launcher.sh standards-compliance
launch-scaffolder config validate ./stapeln-launcher.sh
# Legacy launcher only: current @launcher-deed blocks are intentionally read-only.
launch-scaffolder config set ./legacy-launcher.sh version 0.2.0
# For current launchers, edit the source config and re-mint.Both the native Rust provisioner and generated-shell fallback refuse to replace
or remove an existing unmarked launcher or desktop entry, even with --force.
Each desktop/launcher file is checked independently; installed icons carry a
sidecar ownership marker. Move or inspect a conflicting file rather than
expecting the tool to claim it automatically. Writes use same-filesystem atomic
replacement.
Generated <app>-launcher.sh scripts carry their own --integ /
--disinteg arms (the original, pure-bash implementation). They now
also embed the absolute path of their source config as CONFIG_FILE=…
and, on invocation, fast-path back to launch-scaffolder
provision --integ "$CONFIG_FILE" when the binary is on
PATH. If the binary isn’t present, the in-script shell fallback runs
instead. This means: the Rust binary is authoritative when available,
without making itself a hard dependency for integrated launchers.
launch-scaffolder/ ├── Cargo.toml # Workspace root ├── crates/ │ ├── launcher-common/ # Shared library — all real logic │ │ └── src/ │ │ ├── lib.rs # Public API │ │ ├── deed.rs # DEED v1.0.0 parser (estate format) │ │ ├── standard.rs # Parse launcher-standard_praxis.deed │ │ │ # + LauncherStandard::resolve (flag → │ │ │ # env → the deed's own :priority │ │ │ # ladder → baked fallback) │ │ ├── config.rs # Parse <app>.launcher.a2ml │ │ ├── template.rs # Render via Tera (embeds CONFIG_FILE) │ │ ├── discovery.rs # Walk + prune + fixture-suffix filter │ │ │ # (shared by realign + provision) │ │ ├── integration.rs # Native Rust .desktop writer, │ │ │ # icon/launcher install, gio, │ │ │ # update-desktop-database │ │ ├── metadata_block.rs # Reads DEED and legacy metadata; edits legacy blocks │ │ ├── platform.rs # Planned platform helpers (not wired yet) │ │ ├── integrity.rs # Planned integrity manifests (not wired yet) │ │ └── exceptions.rs # Planned exception merge (not wired yet) │ └── launcher/ # Thin CLI binary │ └── src/ │ ├── main.rs # clap dispatch │ ├── cmd_mint.rs # ✓ working │ ├── cmd_realign.rs # ✓ working │ ├── cmd_provision.rs # ✓ working (native Rust; option b) │ ├── cmd_config.rs # ✓ working │ └── cmd_standard.rs # standard show / validate ├── standards/ │ └── launcher-standard_praxis.deed # Vendored/baked standard fallback ├── templates/ │ └── launcher.sh.tera # Bash launcher template rendered by Tera ├── examples/ │ ├── README.adoc # Fixture-vs-live naming convention │ └── stapeln.launcher.fixture.a2ml # Worked example (fixture suffix!) ├── docs/ │ ├── launcher-exceptions-2026-04-10.adoc │ ├── compliance-audit-2026-04-10.adoc │ ├── branch-protection-remediation-2026-04-10.adoc │ └── ruleset-audit-2026-04-10/ # Audit artefacts (read-only record) └── crates/launcher-common/tests/ # Parser, renderer, fixture and integration tests
To distinguish test fixtures from live, estate-owned launcher configs, file-name suffixes carry the distinction — directory names do not:
| Purpose | File-name suffix | Picked up by estate walks? |
|---|---|---|
Live per-app config |
|
Yes |
Fixture / worked example |
|
No |
The discovery code in `launch-scaffolder-common
discovery
is_live_config` enforces this rule. Fixture files can live anywhere —
including inside a consumer repo’s examples/ directory — without
being swept up by realign or provision --all. See
examples/README.adoc for the full contributor-facing version of the
rule.
Per the hyperpolymath language policy (canonical policy):
-
Rust is the preferred language for CLI tools — zero-dep binary, fast cold start, strong types, excellent ecosystem (clap, tera, sha2, anyhow).
-
“Rust” always means “Rust with SPARK integration as the default stance”. This project is Rust-primary and designed in that direction, but it has no SPARK/FFI implementation today; the
integritymodule is still a placeholder.
A generated launcher writes two files: a pid file and a log. Unless the
config says otherwise ([runtime] pid-file / log-file) they land
under the invoking user’s own XDG directories, never in shared,
world-writable space:
| File | Default | Overridden by |
|---|---|---|
pid |
|
|
log |
|
|
The log goes to the state directory rather than the runtime directory
because it has to survive a logout, which $XDG_RUNTIME_DIR does not
promise. The unique per-app default directories are created with mode
0700. Explicit override paths are never chmodded; the generated launcher
refuses a state directory that is not user-owned or is group/world-writable,
so a legacy /tmp override cannot change /tmp permissions or expose a PID
file to other users.
Before 2026-09-25 both defaults were /tmp/<app>-server.{pid,log}:
world-writable, and predictable from nothing but the app name, so any local
user could create or symlink the path before the launcher’s first run and
influence what it later killed or removed (issue #48). Launchers minted
before that date keep their old paths until they are re-minted — set the
two keys explicitly, or re-mint, to move them. The worked example now leaves
those overrides unset so it demonstrates the secure defaults.
-
A launcher carries a metadata block in its own header, so a generated script can be parsed back by this tool to recover its config and be re-minted. That round-trip is the reason the inputs are declarative rather than a pile of CLI flags.
-
The standard is a praxis DEED (
launcher-standard_praxis.deed, DEED v1.0.0) as of the D73-C ruling — seeADR-010in.machine_readable/6a2/META.a2ml. -
The per-app config (
<app>.launcher.a2ml) and the emitted@a2ml-metadatalauncher header are still on the retired A2ML naming and are parsed as TOML. They are deliberately not converted here: the config conversion is tracked asstandards#960, and changing the emitted header is a wire-format change betweenmintandrealignthat would strand every launcher already on disk. A2ML is retired estate-wide — nothing new should be built on it, and the earlier plan to migrate toa2ml-rsno longer applies.
Alpha. The five CLI subcommands (mint, realign, provision, config,
standard) are implemented; platform/integrity/exception-library modules and
non-Linux integration remain planned. Last reviewed 2026-09-26.
Implemented:
-
✓ Cargo workspace layout (two crates:
launch-scaffolder-common
launch-scaffolder) -
✓
deedmodule — parses the estate’s DEED v1.0.0 format -
✓
standardmodule — parseslauncher-standard_praxis.deed;LauncherStandardwalks flag → env → the deed’s own
resolve:priorityladder → baked -
✓
configmodule — parses<app>.launcher.a2ml(TOML) with runtime-kind validation -
✓
templatemodule — Tera renderer with full context; embedsCONFIG_FILEfor in-script delegation -
✓
discoverymodule — shared walk + prune + fixture-suffix filter -
✓
integrationmodule — native Rust.desktopwriter, icon/launcher install, best-effortgio+update-desktop-database(Linux only; macOS/Windows returnIntegError)
UnsupportedPlatform -
✓
metadata_blockmodule — reads current@launcher-deedand legacy@a2ml-metadatablocks; safe scalar editing remains legacy-only -
✓
templates/launcher.sh.tera— parameterised overruntime_kind∈{server-url,process,remote};--integ/--disintegfast-path tolaunch-scaffolderprovisionwhen on PATH -
✓
mintsubcommand — positional config,-o/--out,--stdout,--no-chmod -
✓
realignsubcommand — estate walk with--search-rootoverride,--dry-run,--check(CI),--keep-going, walk-error tolerant -
✓
provisionsubcommand —--integ/--disintegwith--all,--force,--no-confirm,--dry-run; bulk mode prompts before touching$HOME -
✓
configsubcommand —get/set/validate;setsafely edits legacy blocks and refuses current DEED blocks -
✓
standardsubcommand —show/ semanticvalidate -
✓ Generated launcher implements standard
--versionoutput and routes--browser/--webto--auto -
✓ Unit and integration tests run by locked Rust CI; launcher-artifact CI enforces a 59-test floor, a current golden mint, and ShellCheck
-
✓ 7 launchers fully managed: aerie, burble, game-server-admin, nqc, panll, project-wharf, stapeln
-
✓ 5 declared exceptions documented in
docs/launcher-exceptions-2026-04-10.adoc -
✓ Fixture-vs-live file-naming convention documented and enforced
-
✓ All 7 managed launchers regenerated with template delegation arms (2026-04-10)
Remaining work (see .machine_readable/6a2/STATE.a2ml for the current checkpoint):
-
✓ Mint output is currency-locked against the committed DEED fixture in unit tests and CI; seven-consumer golden coverage remains future work
-
❏ macOS and Windows integration backends in
launch-scaffolder-common::integration -
❏ SPARK integration hook for a future implemented
integrityAPI via Zig FFI -
✓
standardsubcommand —showand semanticvalidate -
❏
platform,integrityandexceptionslibrary APIs — currently placeholders and explicitly not used by the CLI -
❏ Cross-platform CI (Linux/macOS/Windows matrix); current CI runs on Linux
-
❏ Migration of the 5 declared exceptions once the template grows custom-mode hooks
# Standard cargo workflow
cargo build # debug build
cargo build --release # optimized, stripped, single-file binary
cargo test # run tests
cargo run -- --help # invoke the binary
# Or via Justfile
just build
just test
just install # cargo install --path crates/launcherThis project is licensed under the Mozilla Public License, v. 2.0. See
the LICENSE file for details.
SPDX-License-Identifier: CC-BY-SA-4.0
Jonathan D.A. Jewell (hyperpolymath)
j.d.a.jewell@open.ac.uk
Each consumer repo owns its own <app>.launcher.a2ml config at its
repository root. launch-scaffolder mint writes the generated
<app>-launcher.sh next to it. The pre-2026-04-10 pattern of pooling
launchers under /var/mnt/eclipse/repos/.desktop-tools/ is
deprecated; ~/Desktop/Shortcuts/*.desktop files have been repointed
at the new per-repo paths.
Scaffolder-managed consumers (as of 2026-04-10):
-
stapeln — Visual Container Stack Designer (server-url, port 4010)
-
burble — WebRTC voice + control plane (server-url, port 4020)
-
aerie — network diagnostic suite (process)
-
game-server-admin — multi-game server orchestration (process)
-
nqc — NextGen Query Client, inside
nextgen-databases/(process) -
panll — panels framework (server-url, port 8000)
-
project-wharf — container/workload staging (process)
Declared exceptions (still hand-written; see
docs/launcher-exceptions-2026-04-10.adoc for reasoning and migration
triggers):
-
hypatia,invariant-path,opsm,ambientops,idaptik
The bulk-realignment goal: once realign is implemented, one command
will re-mint every managed launcher in the estate against the current
standard.