Build cross-platform desktop launchers from a declarative A2ML spec.
launch-scaffolder is a single Rust binary that generates, installs,
and maintains cross-platform desktop launcher scripts for any
hyperpolymath project (or any other project that adopts the
hyperpolymath launcher standard). It turns launcher scripts into
generated artefacts, not hand-edited files.
Inputs:
-
launcher-standard.a2ml— a declarative description of what a compliant launcher looks like (modes, file paths, permissions, integrity requirements, per-platform behaviour). The canonical copy lives in thestandardsmonorepo at/var/mnt/eclipse/repos/developer-ecosystem/standards/launcher/launcher-standard.a2ml. At runtime the loader resolves in this order:-
--standard<file>CLI flag -
$LAUNCH_SCAFFOLDER_STANDARDenvironment variable -
The canonical path above
-
A baked-in copy compiled into the binary at build time (fallback only)
-
-
<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.
All four working 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
# ─── config ────────────────────────────────────────────────────────────────
# Inspect or edit the `@a2ml-metadata` block embedded at the top of any
# generated launcher script.
launch-scaffolder config get ./stapeln-launcher.sh version
launch-scaffolder config get ./stapeln-launcher.sh standards-compliance
launch-scaffolder config validate ./stapeln-launcher.sh
launch-scaffolder config set ./stapeln-launcher.sh version 0.2.0
# NOTE: `config set` rewrites the generated script in place and warns
# that `realign` will overwrite the change. The durable fix is to edit
# the source <app>.launcher.a2ml and re-mint.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
│ │ ├── standard.rs # Parse launcher-standard.a2ml
│ │ │ # + LauncherStandard::resolve (shared
│ │ │ # 3-step precedence: flag → canonical
│ │ │ # → 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 # Parser + in-place rewriter for the
│ │ │ # embedded @a2ml-metadata block
│ │ ├── platform.rs # Linux/macOS/Windows dispatch (stub)
│ │ ├── integrity.rs # SHA-256 manifests (stub)
│ │ └── exceptions.rs # Standard + config + exception merge (stub)
│ └── 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 # stub
├── standards/
│ └── launcher-standard.a2ml # Canonical standard (baked into binary)
├── templates/
│ └── launcher.sh.tera # Bash launcher template rendered by Tera
├── examples/
│ ├── README.md # Fixture-vs-live naming convention
│ └── stapeln.launcher.fixture.a2ml # Worked example (fixture suffix!)
├── docs/
│ ├── launcher-exceptions-2026-04-10.md
│ ├── compliance-audit-2026-04-10.md
│ ├── branch-protection-remediation-2026-04-10.md
│ └── ruleset-audit-2026-04-10/ # Audit artefacts (read-only record)
└── tests/
└── regression/ # Golden-file regression fixtures (planned)
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 | <app>.launcher.a2ml |
Yes |
| Fixture / worked example | <app>.launcher.fixture.a2ml |
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.md
for the full contributor-facing version of the rule.
Per the hyperpolymath language policy (see
standards/rhodium-standard-repositories/spec/LANGUAGE-POLICY.adoc):
-
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 tool is Rust-primary now, with SPARK/Ada hooks planned for the correctness-critical
integrity.rspath (called via Zig FFI per the hyperpolymath ABI/FFI standard).
-
A2ML is the hyperpolymath standard format for machine-readable config and metadata, and every launcher already carries an A2ML metadata block in its header. Using A2ML end-to-end means a launcher script can be parsed by this tool to extract its config and re-minted.
-
v0.1 uses TOML as the concrete syntax (A2ML is "TOML-like" per the standards
.claude/CLAUDE.md). v0.2 switches to proper A2ML once thea2ml-rsparser reaches feature parity.
Alpha, ~65% complete. Four of five subcommands fully wired end-to-end;
one remains a stub (standard). Last updated 2026-04-10 (phase
phase-4-config-inspector).
Implemented:
-
Cargo workspace layout (two crates:
launch-scaffolder-common+launch-scaffolder)* [x]
standardmodule — parseslauncher-standard.a2ml; `LauncherStandard
resolve` hosts the shared 3-step precedence (flag → canonical → 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* [x]
integrationmodule — native Rust.desktopwriter, icon/launcher install, best-effortgio+update-desktop-database(Linux only; macOS/Windows return `IntegError
UnsupportedPlatform`) -
metadata_blockmodule — hand-rolled parser and in-place rewriter for the embedded@a2ml-metadatablock -
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;setpreserves column alignment in the embedded metadata block -
17 unit tests passing across both crates
-
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.md -
Fixture-vs-live file-naming convention documented and enforced
-
All 7 managed launchers regenerated with template delegation arms (2026-04-10)
Remaining work (ordered by current priority in STATE.a2ml):
-
Golden-file regression tests pinning mint output for the 7 managed launchers
* [ ] macOS integration backend in `launch-scaffolder-common
integration` -
SPARK integration hook for
integrity.rsvia Zig FFI -
standardsubcommand — still a scaffold stub -
platformmodule — still a stub (runtime dispatch handled inside the generated script today) -
integritymodule — still a stub (pending SHA-256 manifest generator) -
exceptionsmodule — still a stub (per-app override merge) -
Cross-platform CI (Linux/macOS/Windows matrix)
-
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.md 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.