choreographr 0.2.3

Agentic coding assistant — daemon, TUI, and bridges
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
# Choreographr — `just` task runner
#
# One-stop entry point for building, testing, linting, running, and releasing
# the workspace. Bare `just` lists all recipes.
#
# Requirements:
#   - cargo          Rust toolchain >= 1.94.1 (the workspace MSRV — see Cargo.toml)
#   - zig            builds `zlob`, the glob/walker dependency of choreo-daemon
#                    (install with `brew install zig` on macOS, `apt install zig`
#                    on Debian/Ubuntu, or from ziglang.org)
#   - cargo-nextest  optional but recommended: the primary test runner
#                    (install once with `just install-nextest`; every nextest-backed
#                    recipe fails with a hint until it is on PATH)
#   - git-cliff      release tooling: builds the release notes from commit
#                    messages (`just release-notes`). Required by the release
#                    gate; install once with `just install-git-cliff`
#
# The recipes mirror the README "Testing & development" section and the
# AGENTS.md pre-commit workflow (fmt + clippy + full test suite).

set shell := ["bash", "-euo", "pipefail", "-c"]

# ── configuration ─────────────────────────────────────────────────────────────

# Cargo build profile used by `build`, `check`, and the `run-*` recipes.
# Defaults to release (matching the README quick start); override for faster
# local iteration with: `just --set profile debug build`
profile := "release"

# Extra flags appended to every cargo invocation. Read from the environment so
# CI can inject e.g. `CARGO_FLAGS="--offline"` without editing this file.
CARGO_FLAGS := env_var_or_default("CARGO_FLAGS", "")

# Cross-target gates (check-macos / check-windows) shared flags. The workspace
# pins `-C target-cpu=native` in [profile.dev] (root Cargo.toml) for host speed,
# but `native` expands to the *host's* CPU, which a foreign target rejects (e.g.
# `znver3` on x86 for aarch64) — so the cross gates must not inherit it. Two
# prongs, because the two consumers read rustflags differently:
#   * cross_config clears the profile's rustflags for the actual cargo build —
#     that is where the workspace's native flag lives.
#   * cross_rustflags (a non-empty RUSTFLAGS; its content is irrelevant to a
#     type-check) masks any *machine-level* rustflags for cargo-zigbuild's
#     `zig cc` probe, which reads them via cargo-config2 and — unlike cargo —
#     ignores `--config`.
cross_config := "--config 'profile.dev.rustflags=[]'"
cross_rustflags := "-Cdebuginfo=0"

# ── entry points ──────────────────────────────────────────────────────────────

# Show all recipes (default — run with bare `just`)
default:
    @just --list

# Show all recipes (alias of `default`)
help:
    @just --list

# Verify the toolchain: cargo + zig + git-cliff + cargo-zigbuild required,
# cargo-nextest recommended
preflight:
    @echo "==> checking toolchain"
    @command -v cargo >/dev/null 2>&1 || { echo "error: cargo not found — install Rust via rustup (https://rustup.rs/)" >&2; exit 1; }
    @command -v zig >/dev/null 2>&1 || { echo "error: zig not found — install it (choreo-daemon's zlob dependency needs it)" >&2; exit 1; }
    @command -v git-cliff >/dev/null 2>&1 || { echo "error: git-cliff not found — release notes are generated from commit messages (run \`just install-git-cliff\`)" >&2; exit 1; }
    @command -v cargo-zigbuild >/dev/null 2>&1 || { echo "error: cargo-zigbuild not found — check-cross (part of pre-release) needs it (run \`just install-cargo-zigbuild\`)" >&2; exit 1; }
    @command -v cargo-nextest >/dev/null 2>&1 || echo "note: cargo-nextest not found (recommended — run \`just install-nextest\`)"
    @echo "==> toolchain OK: cargo $(cargo --version | cut -d' ' -f2) · zig $(zig version) · git-cliff $(git-cliff --version | cut -d' ' -f2)"

# Install the primary test runner (cargo-nextest). `brew install nextest` on macOS.
install-nextest:
    cargo install cargo-nextest

# Install git-cliff (the release-notes generator). Prebuilt binaries from
# https://github.com/orhun/git-cliff are fine too.
install-git-cliff:
    cargo install git-cliff

# Install cargo-zigbuild (drives the check-cross foreign-target gates). taiki-e's
# prebuilt binaries are also fine and much faster: https://github.com/taiki-e/cargo-zigbuild
install-cargo-zigbuild:
    cargo install cargo-zigbuild

# ── hidden prerequisites ──────────────────────────────────────────────────────

# Fail fast with a hint when zig is missing. zig builds the zlob glob/walker
# dependency of choreographr, so every compile/test recipe needs it.
_require-zig:
    @command -v zig >/dev/null 2>&1 || { echo "error: zig not found — run \`brew install zig\` (choreo-daemon's zlob dependency needs it)" >&2; exit 1; }

# Fail fast with a hint when cargo-nextest is missing. The test-* recipes are
# the README's primary runner; failing here beats cargo's cryptic "no such
# command: test-fast".
_require-nextest:
    @command -v cargo-nextest >/dev/null 2>&1 || { echo "error: cargo-nextest not found — run \`just install-nextest\`" >&2; exit 1; }

# Fail fast with a hint when git-cliff is missing (release-notes generation).
_require-git-cliff:
    @command -v git-cliff >/dev/null 2>&1 || { echo "error: git-cliff not found — run \`just install-git-cliff\`" >&2; exit 1; }

# ── build & check ─────────────────────────────────────────────────────────────

# Build the whole workspace with `profile` (release by default)
build: _require-zig
    cargo build {{ CARGO_FLAGS }} --workspace --profile "{{ profile }}"

# Build the whole workspace in debug mode (fast local iteration)
build-debug: _require-zig
    cargo build {{ CARGO_FLAGS }} --workspace --profile debug

# ── stable builds (escape hatch from the nightly default) ─────────────────────
# The default toolchain is NIGHTLY (see rust-toolchain.toml), so every adhoc
# `cargo` command — including per-crate `cargo build -p choreo-x` / `cargo
# check -p x` / `cargo nextest run -p x` — automatically applies the fast
# per-profile `-Z` flags via [unstable] profile-rustflags (see .cargo/config.toml).
# Those same nightly-only bits HARD-BLOCK stable Cargo, so a stable build is an
# explicit opt-out: these recipes run through scripts/build-stable.sh, which
# temporarily strips the nightly-only config/manifest keys for one command and
# restores them afterwards. Stable compatibility itself is guaranteed by the
# code (no nightly features) and the CI MSRV job, independent of this config.

# Stable build (the `profile` — release by default — plus any extra args)
build-stable: _require-zig
    ./scripts/build-stable.sh build --workspace --profile "{{ profile }}" {{ CARGO_FLAGS }}

# Stable type-check (all targets, no linking)
check-stable: _require-zig
    ./scripts/build-stable.sh check --workspace --all-targets {{ CARGO_FLAGS }}

# Stable unit tests (libtest, serialized — no nextest needed)
test-stable: _require-zig
    ./scripts/build-stable.sh test --workspace {{ CARGO_FLAGS }}

# Publish the 14 crates to crates.io (RELEASE.md Phase 2) with the nightly-only
# per-profile rustflags stripped, so published manifests stay buildable by
# stable consumers (`cargo install choreographr --locked`). Runs
# `cargo release publish --workspace` (dry-run by default; add `-- -x` to
# execute, or pass any other cargo-release args after `--`; add `-- --allow-dirty`
# to skip the wrapper's clean-tree gate — see scripts/publish-stable.sh).
publish-stable args="publish --workspace":
    ./scripts/publish-stable.sh {{ args }}

# No linking, but zig is still required (resolution runs zlob's build script).
# Type-check the whole workspace (all targets) — the fastest CI signal.
check: _require-zig
    cargo check {{ CARGO_FLAGS }} --workspace --all-targets

# macOS cross-compile gate (works from Linux or macOS): type-check every library
# crate for aarch64-apple-darwin using zig's clang+lld via cargo-zigbuild's
# `check` subcommand. Note the direct `cargo-zigbuild` invocation — `cargo
# zigbuild check` would misroute to the build subcommand, because cargo passes
# the subcommand name through (cargo-zigbuild 0.23+).
#
# Libs only, deliberately: the final Mach-O binary link needs the Apple SDK,
# which cannot be redistributed (it fails with "unable to find framework
# 'CoreFoundation'"), so real macOS binaries are built on a macOS host in CI.
# `cargo check` skips linking, so this gate passes — but C build scripts (ring,
# zlob, ckb-vm, onig) DO run, so target-specific C breakage is caught too.
#
# Do NOT add `--all-features`: the `blockchain` feature pulls subxt →
# native-tls → security-framework-sys, whose bindgen step reads Apple's
# Security.framework headers (SDK territory — fails from Linux).
#
# cross_config + cross_rustflags clear the host-arch `-C target-cpu=native`
# the workspace pins in [profile.dev] — without them this gate fails on any
# host whose arch differs from the target (see the cross_config definition).
check-macos: _require-zig
    rustup target add aarch64-apple-darwin
    RUSTFLAGS="{{ cross_rustflags }}" cargo-zigbuild check {{ CARGO_FLAGS }} {{ cross_config }} --target aarch64-apple-darwin --workspace --lib

# Windows cross-compile gate: type-check every library crate for
# x86_64-pc-windows-gnu via zig (MinGW bundled — no mingw install needed).
# Same libs-only rationale and host-arch neutralization as check-macos; this is
# the recipe to iterate the Windows port against. A clean build also surfaces
# the zlob archive-naming quirk (zig emits `zlob.lib`, the windows-gnu target
# wants `libzlob.a`).
check-windows: _require-zig
    rustup target add x86_64-pc-windows-gnu
    RUSTFLAGS="{{ cross_rustflags }}" cargo-zigbuild check {{ CARGO_FLAGS }} {{ cross_config }} --target x86_64-pc-windows-gnu --workspace --lib

# Both foreign-target gates in one command: `check-windows` + `check-macos`.
# This is the local stand-in for the release workflow's windows-msvc and macos
# jobs minus the multi-minute full build/link. It catches `#[cfg(windows)]` /
# `#[cfg(target_os = "macos")]` breakage that the host gate is structurally
# blind to — those blocks are cfg'd out on Linux, so `pre-commit`'s
# clippy/test never even parse them (this is how a Windows-only `Ok(())` vs
# `Result<SigId, _>` mismatch once reached a release build). It is part of
# `pre-release` — the ONLY step there that compiles platform-gated code, so a
# platform break fails fast locally before the pushed `release-workflow-dry-run`
# — but NOT `pre-commit`: it needs zig + cargo-zigbuild and rebuilds the whole
# dependency tree once per target. Run it by hand when touching platform-gated
# code.
check-cross: check-windows check-macos

# ── Android ───────────────────────────────────────────────────────────────────
# Termux is the Android runtime for the four suite binaries: they are pushed
# into Termux's $PREFIX/bin as plain executables. choreo-gui is different —
# on Android it is built by `dx` as a cdylib (APK payload), not a suite binary.

# Cross-build the four suite binaries for aarch64-linux-android via
# cargo-ndk. Requires an Android NDK (ANDROID_NDK_HOME / sdkmanager layout);
# add `emulator=true` for an x86_64 build too. See scripts/build-android.sh.
android-binaries *args="":
    ./scripts/build-android.sh {{ args }}

# Prerequisite check + dry run for `android-binaries` — validates cargo-ndk,
# the NDK, and the rustup Android targets, then prints what would run without
# touching the tree. This is the script's verification path (no NDK needed to
# at least see it fail with an actionable message).
android-check:
    ./scripts/build-android.sh --check

# Build choreo-gui for Android via Dioxus CLI (NOT part of build-android.sh —
# different toolchain: dx drives the Android Gradle/cdylib packaging itself).
# `dx` infers the native renderer on Android; requires the NDK for the final
# link. `--package` scopes the build to the choreo-gui crate (dx reads the
# workspace root; the Dioxus config lives in the crate).
#
# Self-sufficient: the `android` CLI (cmdline-tools 23+) replaces the old
# sdkmanager and auto-accepts licenses, so any SDK packages gradle wants
# (platform, build-tools) are fetched here on first run instead of failing
# mid-build. Root-requiring setup (the ANDROID_HOME/ndk symlink for the AUR
# standalone-NDK layout) is NOT attempted here — one-time setup is validated
# below with exact instructions; everything else runs as the normal user.
#
# JDK note: the dx-generated Gradle build (Gradle 9.1) cannot run on JDK 26+
# — its Groovy build-script compiler rejects class file major version 70
# ("Unsupported class file major version 70"). JAVA_HOME is therefore
# normalized to a supported JDK the same way ANDROID_HOME/ANDROID_NDK_HOME
# are: only when the caller has not already set it, never overriding an
# explicit choice. Preference order: JAVA_HOME → JDK 21 (LTS) → JDK 25 →
# whatever `java` resolves to (with a warning, since a too-new default is
# exactly the failure mode above).
gui-android args="":
    #!/usr/bin/env bash
    set -euo pipefail
    # Read-only env normalization (see build-android.sh): dx/gradle only look
    # for the NDK under ANDROID_HOME/ndk, never via ANDROID_NDK_HOME.
    if [ -z "${ANDROID_NDK_HOME:-}" ] && [ -d /opt/android-ndk ]; then
        export ANDROID_NDK_HOME=/opt/android-ndk
    fi
    if [ -z "${ANDROID_HOME:-}" ] && [ -d "$HOME/Android/Sdk" ]; then
        export ANDROID_HOME="$HOME/Android/Sdk"
    elif [ -z "${ANDROID_HOME:-}" ] && [ -d /opt/android-sdk ]; then
        export ANDROID_HOME=/opt/android-sdk   # read-only fallback; SDK installs need a writable ANDROID_HOME
    fi
    # Normalize JAVA_HOME to a Gradle-9.1-compatible JDK (see the comment
    # above): explicit JAVA_HOME wins, then the known JDK install roots, then
    # the PATH default with a warning. Exported so gradle (a JVM process dx
    # spawns) and the dx JVM toolchain resolution both inherit it.
    if [ -z "${JAVA_HOME:-}" ]; then
        for jdk in /usr/lib/jvm/java-21-openjdk /usr/lib/jvm/java-25-openjdk \
                   /usr/lib/jvm/java-17-openjdk; do
            if [ -x "$jdk/bin/java" ]; then
                export JAVA_HOME="$jdk"
                break
            fi
        done
        if [ -z "${JAVA_HOME:-}" ]; then
            echo "warning: no JAVA_HOME set and no known JDK found under /usr/lib/jvm; gradle will use the default java (must be JDK ≤ 25 — newer JDKs fail with 'Unsupported class file major version 70')" >&2
        fi
    fi
    [ -n "${ANDROID_HOME:-}" ] || { echo "error: no Android SDK found (set ANDROID_HOME)" >&2; exit 1; }
    [ -n "${ANDROID_NDK_HOME:-}" ] || { echo "error: no Android NDK found (set ANDROID_NDK_HOME)" >&2; exit 1; }
    # Validate (read-only) that the NDK the build will actually use exists.
    # dx resolves the NDK via ANDROID_NDK_HOME alone (verified empirically) and
    # does NOT require the ANDROID_HOME/ndk/<version> convention, so requiring
    # that symlink would wrongly reject valid setups like /opt/android-ndk.
    [ -e "$ANDROID_NDK_HOME/source.properties" ] || {
        echo "error: $ANDROID_NDK_HOME does not look like an NDK (no source.properties)" >&2
        exit 1
    }
    # Suppress user-level cargo config rustflags for the Android codegen, the
    # same way scripts/build-android.sh does: ~/.cargo/config.toml typically
    # sets [build] rustflags = -C target-cpu=native (host-CPU flags that poison
    # Android codegen and can even SEGFAULT nightly LLVM on aarch64). When
    # RUSTFLAGS is set, cargo ignores build/target rustflags from config files
    # entirely; an empty-string override is treated as unset and --config
    # build.rustflags=[] does not reliably win, so exporting RUSTFLAGS is the
    # only dependable suppression. Exported (not per-command) so it also covers
    # any nested cargo invocation dx makes.
    export RUSTFLAGS="-C target-cpu=generic"
    # Pre-fetch the SDK packages gradle/dx need if the `android` CLI is
    # available (best effort — if dx wants a different version it will say so
    # and the user can `android sdk install` it explicitly).
    if command -v android >/dev/null 2>&1 && [ -n "${ANDROID_HOME:-}" ]; then
        android sdk install platforms/android-35 build-tools/35.0.0 || \
            echo "warning: could not pre-install SDK packages; dx will report exactly what it needs" >&2
    fi
    exec dx build --platform android --release --package choreo-gui {{ args }}

# ── testing ───────────────────────────────────────────────────────────────────

# Full suite — unit + integration — via nextest in one pass (alias of `test-all`)
test: test-all

# Unit tests via nextest (parallel, every test in its own process)
test-fast: _require-nextest _require-zig
    cargo test-fast

# Unit tests with every optional feature off (the default config) — compiles
# the metrics no-op stubs and the feature-off `--metrics-addr` refusal that the
# `--all-features` recipes never build
# (see .cargo/config.toml for why this guards against stub drift)
test-lean: _require-nextest _require-zig
    cargo test-lean

# Integration tests — the #[ignore] suite — via nextest
test-integration: _require-nextest _require-zig
    cargo test-integration

# Everything: unit + integration via nextest in one pass.
# `--locked` keeps the committed Cargo.lock authoritative on the gate path
# (pre-commit): a stale/regenerated lockfile fails instead of silently
# re-resolving against the live registry (see release.sh for the same control).
test-all: _require-nextest _require-zig
    cargo test-all {{ CARGO_FLAGS }} --locked

# Unit tests via libtest (serialized across binaries; no nextest required)
test-libtest: _require-zig
    cargo test {{ CARGO_FLAGS }} --workspace

# Integration tests via libtest (the #[ignore] suite; no nextest required)
test-libtest-ignored: _require-zig
    cargo test {{ CARGO_FLAGS }} --workspace -- --ignored

# Note: the cargo test-* aliases bake in `--workspace` and reject `-p`, so this
# calls nextest directly (see README).
# Test a single crate with nextest (e.g. `just test-crate choreo-proto`)
test-crate crate: _require-nextest _require-zig
    cargo nextest run {{ CARGO_FLAGS }} -p {{ crate }}

# Run doc tests across the workspace
test-doc: _require-zig
    cargo test {{ CARGO_FLAGS }} --workspace --doc

# Run a shard of the nextest suite (CI parallelism). e.g. `just shard 1/2`
shard part: _require-nextest _require-zig
    cargo nextest run {{ CARGO_FLAGS }} --workspace --partition count:{{ part }}

# Re-run flaky nextest tests up to N times. e.g. `just retry 2`
retry n="2": _require-nextest _require-zig
    cargo nextest run {{ CARGO_FLAGS }} --workspace --retries {{ n }}

# ── lint & format ─────────────────────────────────────────────────────────────

# Format all code in place (rustfmt). Flags live in .cargo/config.toml (`fmt-all`).
fmt:
    cargo fmt-all {{ CARGO_FLAGS }}

# Check formatting without modifying files (tree-stays-clean gate).
fmt-check:
    cargo fmt-all {{ CARGO_FLAGS }} -- --check

# Lint the whole workspace, all targets + all features (flags in `clippy-all`).
clippy: _require-zig
    cargo clippy-all {{ CARGO_FLAGS }}

# Lint with warnings denied — a genuinely clean-clippy gate (any warning fails).
clippy-strict: _require-zig
    cargo clippy-all {{ CARGO_FLAGS }} -- -D warnings

# Auto-apply machine-applicable lints in place; the rest are hand-fixed afterwards.
# `--allow-dirty --allow-staged` let it edit a tree that already holds the in-progress
# change. NOT part of `pre-commit` — the commit gate only VERIFIES lint cleanliness
# (via `clippy-strict`); run this by hand first when you want the autofixable lints
# applied before looping the gate.
clippy-fix: _require-zig
    cargo clippy-all-fix {{ CARGO_FLAGS }}

# Supply-chain gate: deny.toml bans (the 2026-08-20 arrayref@0.3.10 attacker
# versions — RUSTSEC-2026-0260 — plus the six deleted payload crates), RustSec
# advisories, crates.io-only sources, and a local registry-cache scan for the
# deleted malicious .crate files. See scripts/check-supply-chain.sh.
check-supply-chain:
    ./scripts/check-supply-chain.sh

# Preview the release notes, generated from commit messages by git-cliff
# (cliff.toml, via scripts/release-notes.sh). Defaults to the workspace version;
# pass one to preview an upcoming release, e.g. `just release-notes 0.3.0`.
release-notes args="": _require-git-cliff
    ./scripts/release-notes.sh {{ args }}

# Release preflight: the release is cut from `master`, up to date with origin, on
# a clean working tree (RELEASE.md "Preflight"). Read-only — it only inspects git
# state; it never fetches, checks out, or pulls (those are the conductor's move).
check-release-state:
    #!/usr/bin/env bash
    set -euo pipefail
    branch="$(git rev-parse --abbrev-ref HEAD)"
    if [ "$branch" != "master" ]; then
        echo "error: releases are cut from master, not '$branch'" >&2
        exit 1
    fi
    if [ -n "$(git status --porcelain)" ]; then
        echo "error: working tree is not clean — commit or stash first:" >&2
        git status --short >&2
        exit 1
    fi
    # "Up to date with origin" is checked against the LOCAL remote-tracking ref
    # (no network): compare HEAD to where origin/master was last fetched to.
    if git rev-parse --verify --quiet origin/master >/dev/null; then
        behind="$(git rev-list --count HEAD..origin/master)"
        if [ "$behind" -ne 0 ]; then
            echo "error: HEAD is $behind commit(s) behind origin/master — run \`git pull --ff-only\`" >&2
            exit 1
        fi
    else
        echo "note: no origin/master ref found — skipping the up-to-date check" >&2
    fi
    echo "OK: on master, clean, and not behind origin/master"

# Release preflight: confirm the stored crates.io token authenticates, so
# Phase 2's publish cannot die mid-batch on a missing/expired/revoked token.
# `GET /api/v1/me` is cookie-only (crates.io forbids API tokens on it), so the
# script probes the auth RESPONSE instead of expecting a 200 — see the header of
# scripts/check-crates-io-token.sh. Read-only.
check-crates-io-token:
    ./scripts/check-crates-io-token.sh

# Release preflight, final step: publish master and kick the release workflow via
# `workflow_dispatch`. That run builds every platform exactly like a tag does but
# creates NO GitHub release (the `release` job is tag-only: `if: startsWith(ref,
# 'refs/tags/v')`), so the pipeline is exercised on GitHub before Phase 1 ever
# tags — the first real tag is never the first test of the pipeline. Requires an
# authenticated `gh` and push access to origin.
release-workflow-dry-run:
    #!/usr/bin/env bash
    set -euo pipefail
    git push origin master
    gh workflow run release.yml --ref master
    echo "release workflow dispatched (dry run — no release created)"
    echo "watch it with: gh run watch   (or the repo's Actions tab)"

# Install the dependency-policy tool cargo-deny (the authoritative layer of
# check-supply-chain). Without it the script falls back to cargo-audit + a
# literal lockfile scan, which covers advisories but not hard version bans.
install-cargo-deny:
    cargo install cargo-deny

# The commit gate (AGENTS.md → Commit Workflow): prove clippy-clean → full test
# suite → format LAST. `clippy-strict` denies warnings, so any remaining lint
# fails the gate and is hand-fixed (run `just clippy-fix` first if you want the
# machine-applicable lints applied automatically). Formatting runs last, not
# first, so the formatted bytes stay behaviourally identical to the tested bytes.
# Safe to re-run: loop it (fix by hand, re-run) until it passes green, then
# `git commit`. The supply-chain guard is a RELEASE guard — it runs in the
# release workflow, not here.
pre-commit: clippy-strict test-all fmt

# The release gate (RELEASE.md → Preflight) — the single command the conductor
# runs before Phase 1. Every local check, in one pass: the toolchain
# (`preflight`), the git release state (on master, clean, not behind origin), the
# quality gate with its one tree-mutating step — `fmt` — replaced by the
# non-mutating `fmt-check` (alongside `clippy-strict`) — the full test suite, the
# release-only guard `pre-commit` omits (`check-supply-chain`), the foreign-target
# type-check (`check-cross` — the ONLY step here that compiles the
# `#[cfg(windows)]` / `#[cfg(target_os = "macos")]` code the host-target steps
# above never see, so a platform break fails fast locally), the crates.io
# credential check, and finally the GitHub dry run (`release-workflow-dry-run`):
# push master + kick the release workflow so the pipeline is proven before a tag.
# Nothing here edits the working tree. Never use `pre-commit` as a release gate —
# it still rewrites the source with `cargo fmt`.
pre-release: preflight check-release-state fmt-check clippy-strict test-all \
    check-supply-chain check-cross check-crates-io-token release-workflow-dry-run

# ── running ───────────────────────────────────────────────────────────────────

#   just daemon -v            # debug logging
# just daemon "-v -q"       # multiple flags
# Run the daemon (`choreographr`) — default-run selects it from the root package
daemon args="": _require-zig
    cargo run {{ CARGO_FLAGS }} --profile "{{ profile }}" -p choreographr -- {{ args }}

# Run the terminal UI client (its own crate — owns its binary)
tui args="": _require-zig
    cargo run {{ CARGO_FLAGS }} --profile "{{ profile }}" -p choreo-tui -- {{ args }}

# Run the desktop GUI client (its own crate — owns its binary)
gui args="": _require-zig
    cargo run {{ CARGO_FLAGS }} --profile "{{ profile }}" -p choreo-gui -- {{ args }}

# Run the instant-messaging bridge (e.g. `just im telegram`).
# Its own crate (choreo-im) owns the binary — no root feature gating anymore.
im args="":
    cargo run {{ CARGO_FLAGS }} --profile "{{ profile }}" -p choreo-im -- {{ args }}

# Run the ACP bridge for ACP-compatible editors.
# Its own crate (choreo-acp) owns the binary. No _require-zig: these bridge
# crates do not depend on choreo-daemon (the only zlob consumer), so the
# zig requirement the old root-feature-gated recipes inherited is gone.
acp args="":
    cargo run {{ CARGO_FLAGS }} --profile "{{ profile }}" -p choreo-acp -- {{ args }}

# Run any workspace crate's binary. e.g. `just run choreographr -v`
run crate args="": _require-zig
    cargo run {{ CARGO_FLAGS }} --profile "{{ profile }}" -p {{ crate }} -- {{ args }}

# ── release & packaging ──────────────────────────────────────────────────────

# Release version — read from the workspace manifest (the single source of
# truth that scripts/release.sh, the Homebrew formula, and the AUR PKGBUILD
# mirror). Evaluated when `just` loads; the release scripts re-read it.
VERSION := `sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1`

# Host release target — the same target set scripts/release.sh and
# scripts/install.sh hardcode (Linux x86_64 + arm64 — both static musl — plus
# both darwin triples, which release.sh builds in one pass on a Darwin-arm64
# host; smoke-test defaults to the host triple's tarball).
TARGET := `case "$(uname -s)-$(uname -m)" in Linux-x86_64) echo x86_64-unknown-linux-musl ;; Linux-aarch64) echo aarch64-unknown-linux-musl ;; Darwin-arm64) echo aarch64-apple-darwin ;; *) echo unsupported ;; esac`

# The tarball `just release` produces — used as the default for
# `just smoke-test` (built by concatenation: just does not recursively
# interpolate `{{ }}` inside a variable's value, so the pieces must be
# joined here, not nested).
release-tarball := "dist/choreographr-" + VERSION + "-" + TARGET + ".tar.gz"

# Pass extra flags through after `--`, e.g. `just release -- --upload --allow-dirty`.
# Dry-run release: build the four shipped binaries, pack the release tarball,
# write SHA256SUMS, build the .deb/.rpm when the tools are present — never uploads.
release args="": _require-zig
    ./scripts/release.sh {{ args }}

# Build everything AND run `gh release create` (requires gh, a clean tree,
# and a tag).
# Full release: the dry run plus upload.
release-upload: _require-zig
    ./scripts/release.sh --upload

# Release from a dirty tree (CI-style flows that stage files first).
release-allow-dirty: _require-zig
    ./scripts/release.sh --allow-dirty

# Pass flags through, e.g. `just release-tap -- --push`.
# Bump the Homebrew tap formula to the release version — dry-run by default:
# clones choreographr/homebrew-choreographr, rewrites Formula/choreographr.rb
# from the dist/ tarballs (version, urls, sha256 digests), validates, and
# prints the diff. `--push` commits + pushes to the tap repo. Run on the
# Linux box after Phase 4 (macOS tarball staged in dist/).
release-tap args="":
    ./scripts/update-homebrew-tap.sh {{ args }}

# (just parameter defaults are literal, so the fallback is an `if` expression.)
# Smoke-test a release tarball — defaults to the one `just release` just built.
smoke-test tarball="":
    ./scripts/smoke-test.sh "{{ if tarball == "" { release-tarball } else { tarball } }}"

# Build the fat .deb from existing target/dist artifacts (Linux only)
package-deb:
    ./scripts/build-deb.sh

# Build the fat .rpm from existing target/dist artifacts (Linux only)
package-rpm:
    ./scripts/build-rpm.sh

# Build the Termux .deb from existing target/android/arm64-v8a binaries
# (Linux only; build those first with `just android-binaries`)
package-deb-termux:
    ./scripts/build-deb-termux.sh

# Pass flags through, e.g. `just install -- --uninstall`.
# Run the pinned-version installer locally (instead of curl|sh).
install args="":
    ./scripts/install.sh {{ args }}

# ── docs & maintenance ────────────────────────────────────────────────────────

# Build API documentation for the workspace (without dependencies)
doc: _require-zig
    cargo doc {{ CARGO_FLAGS }} --workspace --no-deps

# Build and open the API documentation in the browser
doc-open: _require-zig
    cargo doc {{ CARGO_FLAGS }} --workspace --no-deps --open

# Remove all build artifacts
clean:
    cargo clean

# Update all dependencies to the latest compatible versions
update:
    cargo update

# Show the workspace dependency tree
tree:
    cargo tree {{ CARGO_FLAGS }}