Skip to content

hyperpolymath/launch-scaffolder

Repository files navigation

launch-scaffolder

Build cross-platform desktop launchers from a declarative A2ML spec.

License: PMPL-1.0-or-later Rust 1.85+ Status: alpha Mint: working Realign: working Provision: working Config: working

What it does

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 the standards monorepo at /var/mnt/eclipse/repos/standards/launcher/launcher-standard.a2ml. At runtime the loader resolves in this order:

    1. --standard <file> CLI flag

    2. $LAUNCH_SCAFFOLDER_STANDARD environment variable

    3. The canonical path above

    4. 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).

Why this exists

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-scaffolder realign *.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.

Commands

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 script → binary delegation

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.

Architecture

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)

Fixture-vs-live naming convention

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.

Why Rust/SPARK

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.rs path (called via Zig FFI per the hyperpolymath ABI/FFI standard).

Why A2ML for inputs

  • 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 the a2ml-rs parser reaches feature parity.

Status

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)

  • standard module — parses launcher-standard.a2ml; LauncherStandard::resolve hosts the shared 3-step precedence (flag → canonical → baked)

  • config module — parses <app>.launcher.a2ml (TOML) with runtime-kind validation

  • template module — Tera renderer with full context; embeds CONFIG_FILE for in-script delegation

  • discovery module — shared walk + prune + fixture-suffix filter

  • integration module — native Rust .desktop writer, icon/launcher install, best-effort gio + update-desktop-database (Linux only; macOS/Windows return IntegError::UnsupportedPlatform)

  • metadata_block module — hand-rolled parser and in-place rewriter for the embedded @a2ml-metadata block

  • templates/launcher.sh.tera — parameterised over runtime_kind ∈ {server-url, process, remote}; --integ / --disinteg fast-path to launch-scaffolder provision when on PATH

  • mint subcommand — positional config, -o/--out, --stdout, --no-chmod

  • realign subcommand — estate walk with --search-root override, --dry-run, --check (CI), --keep-going, walk-error tolerant

  • provision subcommand — --integ / --disinteg with --all, --force, --no-confirm, --dry-run; bulk mode prompts before touching $HOME

  • config subcommand — get / set / validate; set preserves 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.rs via Zig FFI

  • standard subcommand — still a scaffold stub

  • platform module — still a stub (runtime dispatch handled inside the generated script today)

  • integrity module — still a stub (pending SHA-256 manifest generator)

  • exceptions module — 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

Build

# 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/launcher

License

PMPL-1.0-or-later. See LICENSE.

All original hyperpolymath code is PMPL-1.0-or-later. Third-party dependencies retain their original licenses; see cargo about output (when added) for the full dependency license audit.

Author

Jonathan D.A. Jewell (hyperpolymath)
j.d.a.jewell@open.ac.uk

Relationship to other hyperpolymath projects

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.

About

Cross-platform launcher minter/provisioner/configurator. Generates compliant desktop launcher scripts from A2ML specs.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors