Skip to content

sysupdate: update the box, and reach it from the pit as /update - #37

Merged
ralyodio merged 1 commit into
masterfrom
update-command
Aug 30, 2026
Merged

sysupdate: update the box, and reach it from the pit as /update#37
ralyodio merged 1 commit into
masterfrom
update-command

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

One command for the three nobody remembers the order of, and /update in the pit.

sysupdate                # apt update, apt upgrade, snap refresh
sysupdate --yes          # ...without stopping to ask
sysupdate --no-snap      # apt only
sysupdate --dry-run      # print the commands, run nothing
$ sysupdate --dry-run
  sudo apt update
  sudo apt upgrade
  sudo snap refresh

Why it is not called update

update was not available, and not only by convention:

  • cli-tools update already means "move this checkout to the current commit"
  • update is in KNOWN_VERBS, and a verb wins over the passthrough — so bin/update.ts would have been unreachable through the dispatcher
  • test/dispatcher.test.ts asserts no command may share a name with a verb, so it would have failed the suite outright

So the command is sysupdate and the pit alias is /update — the same precedent as /names → free-names, and safe because nothing on PATH answers to update.

Three decisions worth reviewing

apt, not apt-get. They are not the same command: apt upgrade installs a package that needs a new dependency, apt-get upgrade holds it back. That difference is how kernels and security updates quietly never land on a box everybody believes is current — the same trap root-ubuntu.sh works around with --with-new-pkgs.

sudo only when not already root. A minimal image may have no sudo at all; asking for it there fails for a reason that has nothing to do with updating anything.

Stop at the first failure — the && the one-liner had. Upgrading against package lists that failed to refresh is pointless, and a snap refresh afterwards only buries the real error further up the scrollback.

Beyond that: a box with no snapd skips that step and says so rather than reporting a failure for something that was never going to run; a box with no apt is refused outright rather than left two-thirds updated; and when the upgrade lands a kernel or libc it names the packages and says a reboot is required, that being exactly the moment people stop thinking about it.

Testing

16 new tests, 576 across the suite, all passing. The plan is built separately from running it, so every decision is tested by asserting on the built argv — nothing is upgraded and nothing is spawned, matching how favicon is tested.

The one path injection cannot cover is the real stdio-inheriting spawner, so I smoke-tested that directly:

exit 0 -> 0
exit 1 -> 1
exit 7 -> 7
missing binary -> SysUpdateError: command not found: definitely-not-a-real-binary
runPlan code -> 3 (want 3)
third step ran? -> no

stdio: 'inherit' rather than captured because both halves need a terminal — sudo prompts for a password on one, apt draws progress on the other. Capturing would hang at the password prompt with nothing on screen to explain why.

Typecheck clean for the new files (the two pre-existing src/free-names.ts errors on master are untouched).

Also

Fixes the alias table in plugins/tools/commands/install.md, which had drifted — it was missing /names.

🤖 Generated with Claude Code

https://claude.ai/code/session_013TerE4nvNU3jvS51nRR6Pd

Updating a machine was three commands nobody could quite remember the order
of: apt update, apt upgrade, snap refresh. This makes it one, and the pit
gets it as `/update`.

Deliberately not called `update`. `cli-tools update` already means "move this
checkout to the current commit", `update` is a dispatcher verb, and a verb
wins over the passthrough -- so a `bin/update.ts` would have been unreachable
through `cli-tools` and would have failed the test that says a command may
never share a name with a verb. `/names -> free-names` is the same precedent.
The alias is safe because nothing on PATH answers to `update`.

Three decisions worth keeping:

`apt`, not `apt-get`. They are not the same command: `apt upgrade` installs a
package that needs a new dependency, `apt-get upgrade` holds it back. That
difference is how kernels and security updates quietly never land on a box
everybody believes is current, and it is the same trap root-ubuntu.sh works
around with --with-new-pkgs.

sudo only when it is not already root. A minimal image may have no sudo on it
at all, and asking for it there fails for a reason that has nothing to do with
updating anything.

Stop at the first failure, which is the && the one-liner had. Upgrading
against package lists that failed to refresh is pointless, and a snap refresh
afterwards only buries the real error further up the scrollback.

A box with no snapd skips that step and says so, rather than reporting a
failure for something that was never going to run; a box with no apt is
refused outright rather than half-updated. When the upgrade lands a kernel or
a libc it names the packages and says a reboot is required -- that being
exactly the moment people stop thinking about it.

The plan is built separately from running it, so the decisions are tested by
asserting on the argv rather than by upgrading anything. The one path that
cannot be faked, the stdio-inheriting spawner, is stdio: 'inherit' because
both halves need a terminal: sudo prompts for a password on one and apt draws
progress on the other.

Also fixes the alias table in the tools plugin, which had drifted -- it was
missing /names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TerE4nvNU3jvS51nRR6Pd
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

16 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 2 | LOW: 9

Severity Rule Location
HIGH sh-remote-script-execution root-ubuntu.sh:139
HIGH sh-remote-script-execution root-ubuntu.sh:2816
HIGH sh-remote-script-execution root-ubuntu.sh:2820
HIGH sh-remote-script-execution root-ubuntu.sh:2969
HIGH sh-remote-script-execution root-ubuntu.sh:3998
MEDIUM redos-nested-quantifier src/domain-free.ts:56
MEDIUM redos-nested-quantifier src/porkbun.ts:506
LOW secret-generic-credential src/credentials.ts:36
LOW insecure-temp-file test/blog.test.ts:73
LOW insecure-temp-file test/blog.test.ts:74
LOW insecure-temp-file test/credentials.test.ts:43
LOW insecure-temp-file test/credentials.test.ts:44
LOW secret-generic-api-key test/credentials.test.ts:208
LOW insecure-temp-file test/download.test.ts:99
LOW insecure-temp-file test/download.test.ts:100
LOW secret-generic-credential test/shorten.test.ts:36

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit b358c8b into master Aug 30, 2026
5 checks passed
@ralyodio
ralyodio deleted the update-command branch August 30, 2026 06:56
ralyodio added a commit that referenced this pull request Aug 30, 2026
Eight commits since 0.15.0, one of them a new command, so the minor moves.

- sysupdate: apt lists, apt packages and snaps in one command, reachable
  from the pit as /update (#37)
- root-ubuntu.sh: a 2G swapfile on a box with no swap at all, plus
  vm.swappiness=10 (#36)
- root-ubuntu.sh: one ssh-agent per user under systemd (#35)
- root-ubuntu.sh: lm-sensors, smartmontools and the rest of the hardware
  inventory set, with sensor detection on bare metal
- companions: diskpush, and installers that are not npm (#34)
- porkbun: Porkbun's own pre-flight, structured refusals, and the fact that
  registration spends prepaid credit rather than a card (#32, #33)


Claude-Session: https://claude.ai/code/session_013TerE4nvNU3jvS51nRR6Pd

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant