tdep-survey is one checkout with a backend and two independent Android
clients, so a project has to be able to declare two Apk components. It
could not: APK_PATTERNS was anchored at the project root, which reached
the first client and stopped, and package, strip, the variant list and
the download all came from AppEntry::apk_component()'s first match --
correct only while a project produced one APK.
Each component's builds are now found under its own cwd, which is what
cwd already meant everywhere else: the directory the build command runs
in, the subtree staleness is scoped to, a server's WorkingDirectory. A
component that declares none sits at the project root, so nothing about
the single-APK case changes. The alternative -- naming the file on the
component -- would have made a component a file path, and an app being a
project path rather than a file path is this project's central invariant.
Everything derived from an APK follows it onto the component: package,
previousPackage, strip, size, mtime, variants and the rename note, in a
nested `apk` block that a Server simply doesn't have. Two clients install
over different packages, so first-wins would have checked the installed
state of one and reported it as the other's -- which looks exactly like a
correct answer. For the same reason a download naming no component is
refused rather than guessed; naming none still answers for a project with
one, which is what lets the frozen /self/apk keep working.
On the phone the per-device state is keyed by project and component, so
the variant picker moved inside the component's own card, beside the
build it picks, and the installed state, size and icon are each their own
component's. A project building two clients shows no single icon of its
own rather than borrowing the first one's.
Measured against the real thing: pointed at tdep-survey, the two clients
resolve to their own builds (15 MB and 153 MB), strip applies only to the
one that asked for it (153 MB served as 52 MB), pressing Install on one
row installed that row's package and left the other row offering Install,
and a download with no component named answers 400 saying which flag to
pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, both about a card being able to act on what it says.
The progress bar, its counts and the last line a component printed now
sit below that component's buttons rather than above them. A bar reports
on the press that started it, so it reads in the order it happened -- and
above, it pushed the buttons down the moment a build began, moving the row
somebody had just pressed out from under their finger.
A pull that cannot fast-forward because the checkout shares no history
with its upstream now offers a way past it. There is no fast-forward
between two unrelated histories and there never will be, so the card was
one that could never be pulled again, with the only remedy on the build
machine -- exactly where the person holding the phone isn't. The card
reports the failure as before and a dialog offers a forced pull, naming
the branch and the upstream it is about to overwrite; confirming sends
?force=true, which resets onto the upstream instead of merging.
Whether it *is* that failure is decided structurally, by `git merge-base`
finding no common ancestor, rather than by matching what git printed:
those messages are translated, and a button that appeared only on an
English build machine would be worse than no button. It travels to the
phone as its own field for the same reason. The dirty-tree refusal stays
in front of it, so a forced pull can only ever discard something that was
committed, and a merely diverged history is not offered it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Rust backend that discovers Android projects under configured roots,
builds one on request, and serves the APK over pinned TLS on a WireGuard
interface; an Android client that lists what is buildable, watches a build,
and installs the result. Enrolment carries the token and the CA, so the
phone trusts exactly the machine that issued it and nothing else.
`AGENTS.md` is the working guide and `README.md` the configuration
reference. The shared tunnel-and-TLS code lives in `vendor/wg-app-link`,
which ai-app uses too.
History before this point was squashed away, and a stale `config.json` went
with it: nothing had read that file since the config moved to RON outside
the checkout, and what it still held was one machine's absolute paths and
the names of projects on it.