607745ecfdcfd4c5610add82a67e14a424cc2bb3
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4043787a58 |
fix: close the four known installer bugs (DU-H1, DU-H2, BUG-1, BUG-2)
- DU-H1: flags are parsed wherever they sit, so `install --unattended` and `--unattended install` are the same run; any unknown flag, word, or subcommand exits 2 to stderr before a package manager is touched. - DU-H2: every download lands in one private mktemp -d (mode 700) workdir per run, is checked non-empty before sudo tar sees it, and an EXIT/INT/TERM trap cleans up. No fixed /tmp paths remain. - BUG-1: ^t is now toggle-shown — it ticks only the rows the active filter is showing, and @needs expansion stops at the first invasive row, so an invasive package can never be ticked off-screen. - BUG-2: ^t journals what it added, so a second ^t over the same shown set unticks exactly that set; the bind no longer clears the query. - lab: the type verb polls fzf's reported query to a deadline instead of a fixed sleep; marks_settled retries within its deadline. Suite 256/0 host, 214/0 docker (ubuntu:24.04), mutations 24/24 killed (six new mutants re-introduce each bug and all die), lab 6/6 green. |
||
|
|
1ea6b492eb |
fix: dotup — installer, picker and private-tier correctness pass
Installer: - channel order is a dependency graph: `script` now runs before `brew`, so core/brew exists by the time lazygit, omp and herdr are attempted. Those three have no apt package at all and failed on every fresh Linux box. - core/brew and core/node are real script/tarball rows now. apt's nodejs is 18.19 while four npm rows declare node>=20; npm only warns on a failed engines check, so the install "succeeded" and `pi` then died at parse time. - apt_known asks `apt-cache policy` for an installation candidate rather than `apt-cache show` for existence: docker-ce exists in the cache with `Candidate: (none)` and exits 0, which kept it in the batch and made apt refuse all thirty packages at once. - failures are reported by manifest key, not by install argument, so the name in the summary is one you can type at the picker or pass to `dotup explain`. - flatpak installs the tool, adds the flathub remote, and uses --user, which is the only scope that works without a session bus. - gh: deb assets fill %a and %v, because a distro-targeted deb names the release as well as the arch (ghostty-ubuntu). - ensure_bws downloads into a `mktemp -d` instead of fixed /tmp paths. - the endpoint password prompt says so out loud when stty cannot turn echo off, instead of silently leaving the password in the scrollback. - cmd_toggle writes tmp-then-rename, matching cmd_expand. Picker: - DU-C1: comments had been inserted between the continued lines of the fzf invocation. The `\`-newline is stripped first, so the comment's own newline terminated the command — fzf ran with two binds and the lines below it ran as a command named `--bind`, so `dotup pick` returned 1 having drawn a dead picker. The comment now sits above the pipeline. - the header is built with a plain variable: `$'...'` is a bashism and /bin/sh on Ubuntu is dash, the one platform this is written for. - the /dev/tty probe runs BEFORE the defaults preset, so a run in a pipe no longer overwrites the selection with 43 rows before failing to draw. - a completed pick is recorded in $STATE/picked, so "install nothing" survives the next run and a prior `dotup plan` no longer counts as having chosen. - $SEL must be readable and writable before anything draws: every bind is execute-silent, which throws its child's status away. - ensure_fzf resolves system-first, cache-second, as the README promises. - ^t clears the query, so rows pulled in along @needs are on screen. |
||
|
|
f30dddce15 |
fix: resolve chezmoi instead of assuming it is on PATH
The private tier died with `chezmoi: not found` on a real install, AFTER the
bws token had been written -- half-configured, at the last step, password
already spent.
dotup arrives VIA chezmoi, so "it must be here already" is the natural
assumption. It is wrong: get.chezmoi.io installs to ./bin relative to the CWD
when -b is not given, which is exactly what the README's one-liner does. Run it
from $HOME and the binary is ~/bin/chezmoi; run it from /workspace, as anyone
in a container does, and it is /workspace/bin/chezmoi. Neither is on PATH, and
~/.local/bin is not either in bash -- the same gap that makes bare `dotup` fail
on a fresh box.
find_tool exists for precisely this and the call site bypassed it.
- find_tool now also searches $HOME/bin
- ensure_chezmoi resolves it, and installs to ~/.local/bin only if it truly
is absent, mirroring ensure_bws
- the init uses "$CHEZMOI", never the bare name
- resolution happens BEFORE the credential prompt, so this fails while it is
still free rather than after the password is spent
Verified by reproducing the exact scenario: installer run from /workspace, so
chezmoi lands in /workspace/bin and nothing on PATH can see it. Old code:
`chezmoi: not found`. New: detected, installed, prompt reached. Also confirmed
~/bin/chezmoi is found WITHOUT re-downloading, so the common case costs nothing.
113 -> 117.
|
||
|
|
53022b8146 |
fix: a wrong password no longer costs an entire reinstall
The endpoint credentials are asked for at the very end of a run, after every package is installed -- correct, because a password typed at picker time would sit in memory through ten minutes of downloads. But any non-200 was fatal, so one mistyped character meant repeating the whole install to get back to a three-line prompt. Now it retries, up to five attempts. URL and username persist across them and blank keeps them, shown as `Bootstrap URL [https://...]:`, so only the password is retyped -- retyping an address that was already pasted correctly is its own source of error. `q` at the URL prompt leaves the machine public-only. The message also names the fault. Every failure used to print "endpoint refused the credentials", including a 404 and an unreachable host -- which is precisely how a URL-shape bug reads as a password problem. The status is now captured alongside the body rather than relying on --fail: 401 wrong username or password 404 reached the host, but no bootstrap.env is there -- check the route 000 could not reach that address (DNS, TLS, or the host is down) The body is used only when the code is 200, so an error page is still never parsed as a blob; that was --fail's job and an explicit check is stronger. `dotup private` already re-ran only this step, leaving installed packages alone -- it was simply missing from --help, so the cheap way back in was undiscoverable. Documented, and the failure path now points at it. The end-to-end sends a wrong password first on purpose and asserts both the message and that blank-blank-correct works, which is the only way to test a recovery path. Note that harness clones the PUBLISHED repo, so it validates what a user gets, not the working tree -- these invariants are covered in the unit suite, which reads the tree directly. 105 -> 113. |
||
|
|
2afdfd093b |
fix: accept either bootstrap URL shape, in dotup and in the harness
Three files each appended /bootstrap.env to a value whose shape nobody had
pinned down, and they did not agree:
- the rotation scripts print the FULL file URL and say to store THAT in
Bitwarden, so that is what gets pasted
- dotup asked for the directory and appended /bootstrap.env itself
- .tests/e2e.sh independently duplicated dotup's assumption
Pasting the saved value made the request .../bootstrap.env/bootstrap.env. The
404 tripped curl --fail and surfaced as "endpoint refused the credentials. The
machine stays public-only" -- blaming the password for a URL shape, which is
about the most expensive wrong error message this path could produce.
dotup and the wrapper now both strip a trailing slash and a trailing
/bootstrap.env before building the request, so either form works and the
existing Bitwarden entry needs no editing.
Four assertions cover all four shapes -- directory and file URL, each with and
without a trailing slash. They eval the normalisation lifted straight out of
dotup rather than a copy, and that binding was mutation-tested: deleting the
line from dotup makes them fail with exactly the production symptom,
https://h/r/bootstrap.env/bootstrap.env. 101 -> 105.
The second instance, in e2e.sh, was found only by running the harness with the
file URL instead of the directory. Every earlier run passed because it was fed
the shape that happened to be in a variable, not the shape a person has in a
password manager. Both shapes now run end to end and pass.
The normalisation exists in two places because the harness cannot source
dotup's internals. That is the same duplication that caused this, and only
dotup's copy is covered by the suite.
|
||
|
|
63bed1d32d |
fix: keep the gitea token out of chezmoi init's argv and .git/config
The endpoint hands back a clone URL with the read-only token inline, and that
URL went straight onto `chezmoi init`'s command line. Two durable exposures
followed, neither of which the existing redaction touched -- it only kept the
token out of the trace line:
- /proc/<pid>/cmdline is world-readable, so any account on the box could read
the live token for as long as the clone ran
- the resulting .git/config recorded the credential-bearing remote and kept
it until the tree was deleted
Split the credential out of the URL before anything executes. The token goes to
$STATE/private-credentials at 600 in git-credential-store format; the URL that
reaches argv and .git/config is clean. The helper is passed via GIT_CONFIG_*
for the clone and then written into the clone's own config, so a later
`chezmoi update` still authenticates without the token being stored.
Verified: clone succeeds with the clean URL, .git/config contains no token, and
a subsequent fetch authenticates from the credential file alone. Suite 101/101.
|
||
|
|
68e16ad1ff |
fix: pin the bws version instead of resolving it
ensure_bws resolved the version from the GitHub releases list API. That worked on the dev box and failed in a bare container with "could not resolve the current bws version" — because the dev shell had `gh` authenticated and the container did not. Unauthenticated, that endpoint returns an empty array for this repo. The obvious fallback does not work either: sdk-sm is a monorepo with per-component tags, so /releases/latest redirects to python-v2.1.0 rather than any bws release. Release-download URLs need no auth, so BWS_PIN=2.1.0 and the published sha256 makes bumping it safe. Same pattern as FZF_PIN. |
||
|
|
400bd9b9f1 |
fix: install bws from the private tier, not the manifest
The previous commit put the bws install on the private/bws-secrets row as a tarball channel. That row can never install anything: selected_packages drops every row flagged `private`, which is the invariant that makes --unattended safe to run. The container proved it — unzip installed, bws did not, and the row still could not deliver on its promise. Nor can it be a safe row: the defaults preset ticks every safe package, so that would put a Bitwarden binary and a 12 MB GitHub download on every throwaway public VM, for a tool those machines have no credential to use. So ensure_bws lives in dotup and is called from cmd_private immediately after the token is written. Nothing installs bws unless something is about to hand it a token, and the manifest keeps its invariant. core/unzip stays a real package with the @needs edge — the release is a .zip and 24.04 minimal has no unzip. |
||
|
|
90bca39396 |
fix: install bws, isolate the private config, keep credentials out of logs
Three defects the private tier could not survive a fresh machine with. bws was never installed. The private/bws-secrets row promised seven API keys and shipped no way to fetch them: dotsecrets shells out to `bws secret get`, and nothing put that binary on the box. Added as a tarball channel with checksum verification, musl rather than gnu so it does not pin a glibc newer than an older LTS carries, plus core/unzip as a real @needs dependency since the release is a .zip and 24.04 minimal has no unzip. The releases API needs filtering by tag prefix: sdk-sm is a monorepo and `latest` usually points at a python SDK, not bws. Both tiers rendered their config to ~/.config/chezmoi/chezmoi.toml, so re-running the public installer overwrote the private config and took its seven promptStringOnce answers with it. Silently -- the templates degrade politely when data is missing, so the symptom was `git commit` not knowing who you are, days later. The private tier now renders to private.toml and the cmp alias carries the matching -c. `run` echoes its argv to stderr, and the private init argv ends in https://user:TOKEN@host -- into scrollback, any `dotup 2>log`, and any agent transcript. Traced through redact_url instead, in both the live and dry-run branches. The clone is also chmod -R go-rwx afterwards: it lands at the caller umask, and .git/config stores that same credential URL. Tests: 90 passing, 9 new covering all three. |
||
|
|
b487b0e855 |
feat: public dotfiles tier — no credential, no identity, one installer
Fresh history. This is the repo a throwaway VM clones anonymously: it brings a
machine to a working baseline and carries nothing that makes it mine.
56 files. 50 land in $HOME, 3 are chezmoi metadata, 2 are repo documentation,
1 is the manifest, and a 15-file test harness stays behind in .tests/.
What did not travel, and why:
encrypted_private_bws-token.age a real credential; age is dropped entirely
.chezmoidata/bws.toml env-var -> secret-id map; belongs with the
tier that can use it
SECRETS.md documentation of the rules, not config
finish-setup.sh.tmpl superseded by dotup
nvim/init.lua.backup dead file
dot_claude/**, dot_codex/**, 120 files of agent config, private tier
dot_pi/**
De-identified rather than dropped:
.gitconfig [user], the GitHub ssh rewrite and both Gitea host rewrites are
identity, not configuration. They move behind an [include] of
~/.config/git/config.local, which the private tier writes. Git
treats a missing include as a no-op, so a public-only machine
reads the file and stops.
.zshrc the two gitea aliases carried a personal domain and a LAN IP.
They move behind a guarded source of ~/.config/zsh/local.zsh,
the sibling of the secrets.zsh seam phase 2 established.
nvim a commented-out LM Studio endpoint naming a LAN address.
ghostty a stale auto-generated header naming an absolute home directory.
Newly captured, never tracked before: ~/.zshenv, ~/.config/gh/config.yml. The
former sourced ~/.cargo/env unguarded, so every zsh on a machine without rustup
printed an error -- the same shape as the unguarded oh-my-zsh source phase 2
fixed. It is guarded now.
.chezmoiexternal.toml grows from one entry to six. oh-my-zsh, powerlevel10k,
zsh-autosuggestions, zsh-ai and tpm were hand-installed and declared nowhere,
which is why `chezmoi init --apply` on a clean box produced a .zshrc that broke
the shell it configures. The theme and both plugins nest under
.oh-my-zsh/custom/, which is what $ZSH_CUSTOM resolves to.
dotup gains an install engine. It resolves each selected package to a channel
(apt, brew, npm, uv, snap, deb, flatpak, tarball, script, builtin) through one
function every consumer reads, probes apt-cache before batching so a name apt
does not know moves to brew instead of failing all thirty, and retries
individually if a batch still fails -- which earned its keep on the first real
container run, where mermaid-cli's puppeteer dependency failed and the other
twelve npm packages installed anyway. --unattended computes safe defaults fresh
from the manifest rather than inheriting a state file, and refuses private and
invasive rows outright even when a stale state file ticks them.
The manifest gains @spec, a second directive kind alongside @needs, carrying the
argument a channel needs but a package name cannot supply -- the scoped npm
name, the flatpak app id, the .deb source. The TSV stays five columns wide.
Three bugs the container runs found, all fixed here:
* `apt install nodejs` gives you node WITHOUT npm on Ubuntu, so all thirteen
npm packages failed on a fresh box. The manifest asks apt for both names.
* A tool installed a moment ago is not on this process's PATH -- uv lands in
~/.local/bin, npm -g honours the ~/.npmrc prefix, linuxbrew is outside a
non-login PATH. Resolved by looking in the places we just wrote to, never by
exporting a modified PATH.
* `A || { B && C; }` is one || list, so when `command -v sudo` failed the list
failed and `set -e` killed dotup at load. On a non-root machine with no
sudo it died before printing anything. There is a regression test.
.zshenv and .p10k.zsh are marked private_. Both are shell code the login shell
executes and both applied at 664, group-writable. Third occurrence of the class
of bug phase 1 found on .pi/agent/auth.json and phase 2 found on .zshrc; the
first one found on purpose rather than by accident.
Verification: 81 assertions, 81/81 on this box and in ubuntu:24.04, ubuntu:22.04
and debian:12. The installer is driven against a directory of fake package
managers that record what they were asked to do and install nothing, so the
engine is exercised end to end without a package landing on the test machine.
`gitleaks detect` over the full history and the working tree: no leaks found,
with no allowlist and no .gitleaks.toml.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|