@gjermundgaraba/upup 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Understand, audit and update system tools and project dependencies from one TUI.
8
8
  npm install -g @gjermundgaraba/upup
9
9
  ```
10
10
 
11
- Requires Node 26.4 or newer. The TUI is built on [OpenTUI](https://github.com/anomalyco/opentui), which runs through Node's experimental FFI; the `upup` command enables it for you.
11
+ Requires Node 26.12 or newer. The TUI is built on [OpenTUI](https://github.com/anomalyco/opentui), which runs through Node's FFI.
12
12
 
13
13
  ## Usage
14
14
 
@@ -22,8 +22,14 @@ upup project [dir] # dependencies of the project containing dir
22
22
 
23
23
  **Project** lists the registry dependencies declared across the workspace that contains the directory: Go modules, Cargo workspaces, and npm or pnpm projects including pnpm catalogs. Path, Git and workspace references are not listed. Yarn and Bun projects are reported as unsupported.
24
24
 
25
+ In a git repository it also lists every project below the directory that a `package.json`, `go.mod` or `Cargo.toml` git tracks or doesn't ignore belongs to, each once, its provider named with where it is (`npm clients/voice`). A `package.json` with a lockfile beside it is a project of its own, and so is a Go module its `go.work` doesn't use. Folders under `node_modules`, subtrees vendored in with `git subtree`, and Go modules in the folders the go command skips (`testdata`, and those starting with `_` or `.`) are left out. Node projects below the directory that have no lockfile yet, or use Yarn or Bun, are named together in one notice.
26
+
25
27
  It also lists the GitHub Actions and reusable workflows that the repository's workflows (`.github/workflows`), its own `action.yml` and its composite actions (under `.github/actions`, and any local action a workflow uses) use, once per action and release however many files say it. Their tags are read with `git ls-remote`: first from the server the repository's `origin` is on, when that isn't github.com, since a GitHub Enterprise Server runs its own copy of an action before github.com's; then from github.com, a private repository's with your own git config. An update moves to the newest release tag written like the current one: as precise, and with or without the `v`. So `@v4` moves to `@v5` once the action tags `v5`, and `@v4.1.2` to the newest full release. A commit pin is read as the release tag that points at it (the one its comment names, such as `# v4.1.2`, else the most precise) and moves to the commit of the newest tag like that one, its comment's version moving with it; upup says when a comment names a tag that points elsewhere. Local (`./`) actions are read rather than listed, Docker actions are not listed, and branches such as `@main` are listed without an update. Removing an action is left to you.
26
28
 
29
+ It also lists the Node.js versions pinned exactly, once per version however many files pin it: `node-version` in the `actions/setup-node` steps of every workflow and action upup reads for GitHub Actions, and the first line that isn't blank or a comment in the `.node-version` and `.nvmrc` files of the directory, the folders below it and those between it and the repository's top that git tracks or doesn't ignore. An update moves to the newest release on the same major line from nodejs.org, keeping each file's `v` or quotes, since moving to another major line is a migration rather than an update. `22` and `lts/*` already follow their line and aren't listed; matrix entries, expressions and Dockerfile `FROM node:` images aren't read yet. Like OS updates, these aren't audited.
30
+
31
+ The package manager a Node project declares, in `packageManager` (`pnpm@12.5.1`) or `devEngines.packageManager`, is listed too, each field as a row of its own, and moves like a dependency to its newest stable release that the project's minimum release age lets through. Its current version is the one pnpm locked, or else the declared one; a range with no locked version isn't listed. Corepack's sha512 hash moves with the version, worked out from the registry's integrity for the release; a pin with another kind of hash is listed without an offer. Neither field can be removed from upup.
32
+
27
33
  Current versions come from the package manager or lockfile. An offered update is the newest stable release. Updating a declared range keeps its style (`^1.2.0` becomes `^1.4.0`), and the lockfile is reconciled and checked afterwards. Removing a project dependency edits the manifest and reconciles the same way.
28
34
 
29
35
  ## OS updates
@@ -40,23 +46,21 @@ Installing needs root. Unless sudo needs no password, upup asks for yours in a p
40
46
 
41
47
  On macOS, upup also lists the apps in `/Applications` and `~/Applications` (and in plain folders one level down) that update themselves, from the same sources [Mole](https://github.com/tw93/Mole) reads, and the same table of feeds for apps that choose theirs at run time:
42
48
 
43
- - **Sparkle** apps, from the appcast their `Info.plist` names (`SUFeedURL`). upup offers the newest stable update on the default channel that this Mac can run. Sparkle compares builds, so unless the versions people see differ, each shows its build too: `1.0 (879)` → `1.0 (918)`.
44
- - **Electron** apps built with electron-builder, from the GitHub or generic feed in their `app-update.yml`: the zip for this Mac in the newest stable release.
49
+ - **Sparkle** apps, from the appcast their `Info.plist` names (`SUFeedURL`). upup offers the update Sparkle itself would install, as Sparkle chooses it: on the default channel, one this Mac can run and that Sparkle updates the installed build to, and the newest that isn't a major upgrade (`sparkle:minimumAutoupdateVersion`) before any that is. That may be a build named as a beta, if the app offers it on its default channel, as its own Check for Updates would; when Sparkle would take an update only to read about, upup offers none. Sparkle compares builds, so unless the versions people see differ, each shows its build too: `1.0 (879)` → `1.0 (918)`.
50
+ - **Electron** apps built with electron-builder, from the GitHub or generic feed in their `app-update.yml`: the zip for this Mac in the newest stable release. On Apple Silicon, when `latest-mac.yml` names no zip for it, upup also reads `arm64-mac.yml` beside it, where some apps, such as Notion, keep their Apple Silicon build, and takes that unless `latest-mac.yml`'s release is newer.
45
51
  - **Mac App Store** apps, at the version their App Store page offers for the Mac. The App Store installs those: press `o` to open the app's page there.
46
52
 
47
53
  Apps a Homebrew cask installed stay Homebrew's, except those of casks that leave updates to the app (`auto_updates`), which are listed here; `d` on one runs `brew uninstall --cask`. upup doesn't remove other apps: move them to the Trash in Finder.
48
54
 
49
55
  Like OS updates, app updates are marked as not audited and install as confirmed even with **audit first** on.
50
56
 
51
- Before anything changes, upup checks each update:
57
+ A Sparkle app's update installs through Sparkle itself, as the app's own Check for Updates would install it: upup-sparkle, a small helper upup ships, runs Sparkle against the app with the feed upup listed, and lets it install only the listed build. Sparkle downloads the update and checks it against the app's EdDSA key (`SUPublicEDKey`), or an older app's DSA key, and, for an app rather than an installer package (such as NordVPN's), its code signature, and keeps the app's owner. As the app's own Check for Updates does, it notes when it last checked and forgets a version you skipped in the app, in your settings, or in root's for an update upup installs as root. upup refuses up front what it can tell Sparkle would refuse once the app had quit, such as an app with a malformed EdDSA key, or with no key at all and a feed that isn't HTTPS, an update the feed doesn't sign for an app with an EdDSA key, and an installer package Sparkle has no key or signature to check by. Sparkle also refuses an installer package through an upup-sparkle signed ad hoc, as one built locally with `vp run sparkle` is, so upup refuses those updates up front; the published package's helper is signed with a Developer ID.
52
58
 
53
- - The download matches its feed's EdDSA signature when the app has a Sparkle key (`SUPublicEDKey`), or the feed's sha512 for Electron.
54
- - The new app is signed through Apple's developer chain as the installed app's bundle ID by its team (`codesign --verify --deep --strict -R`), is built for this Mac, and is the offered version.
55
- - An installer package, for Sparkle updates that come as one (such as NordVPN's), is the only one in the download and is signed with a Developer ID Installer certificate of the installed app's team.
59
+ An Electron app's update upup downloads and checks itself before anything changes: the download matches its feed's sha512, and the new app is signed through Apple's developer chain as the installed app's bundle ID by its team (`codesign --verify --deep --strict -R`), is built for this Mac, and is the offered version. upup then copies the new app beside the old one and swaps them; if the swap fails, it puts the old one back, or says where it is, and the next listing says so too until the old copy (`<app>.upup-old`) is gone.
56
60
 
57
- upup then copies the new app beside the old one and swaps them; if the swap fails, it puts the old one back, or says where it is, and the next listing says so too until the old copy (`<app>.upup-old`) is gone. It never replaces an app while anything runs code from inside it: when the app itself is open, upup asks during the install, one app at a time, whether to quit it, and opens it again afterwards, also when the install failed; a helper or agent still running from inside it fails that update, naming it, its pid and what started it, until you quit or stop it. Nor does it replace an app that changed since upup listed it, such as one that installed its own update as it quit; refresh to see what it offers now. upup won't update the app it runs in, such as your terminal: update that one from another terminal.
61
+ Only the app itself being open is in the way: upup asks during the install, one app at a time, whether to quit it, waits for it to go, and opens it again afterwards, also when the install failed or upup was stopped, failing the update, saying so, if it won't open; a Chromium browser, such as Helium or Chrome, reopens with the tabs it had, as it does after updating itself. If the app opens again before upup replaces it, or hands it to Sparkle, or is open for another user, its update fails. An Electron app's own updater may install an update it had waiting as the app quits; upup waits up to a minute for it to finish, then takes the app as it left it. Anything else running from inside the app, such as a helper, a daemon of yours or a program another app started from its files, is left running, as the app's own updater leaves it: it keeps the old version until it restarts. An update you say no to is skipped. Agents over MCP can't allow quitting an app, so those updates fail for them while the app is open. Nor does it replace an app that changed since upup listed it; refresh to see what it offers now. A Sparkle app with an update downloaded installs it as it quits, or whenever it chooses: if that was the very update upup was about to install, the app counts as updated, and if it was an older one, upup usually checks the app again and installs its update over it; otherwise refresh and update it again. upup won't update the app it runs in, such as your terminal: update that one from another terminal.
58
62
 
59
- Apps upup can't write to, and every installer package, need root. They install together once the others are done, through one sudo command that asks as OS updates do, but separately from them. The password popup comes before any app is quit; a wrong password or a cancelled Touch ID sheet fails those updates and opens again the apps upup quit. As root, upup copies each checked update where only root can write, checks that copy again (the app against its code hashes, a package against its sha256 and Developer ID), and installs only that copy.
63
+ Apps upup can't write to need root, and so do Sparkle updates that are installer packages, and Sparkle apps that another user or a group you aren't in owns, as installer packages leave apps, for which Sparkle would otherwise ask for an admin's password itself. They install together once the others are done, through one sudo command that asks as OS updates do, but separately from them. The password popup comes before any app is quit; a wrong password or a cancelled Touch ID sheet fails those updates and opens again the apps upup quit. As root, upup copies each checked Electron app, and upup-sparkle, where only root can write, checks that copy again (the app against its code hashes and the offered version, upup-sparkle against its sha256), and uses only that copy. Before Sparkle installs as root, upup checks that the installed app, whose key and feed Sparkle reads, is still signed by its team. Stopping upup while Sparkle installs an update may not stop Sparkle from finishing it.
60
64
 
61
65
  macOS may stop upup from quitting an app or replacing one until the app upup runs in is allowed under System Settings › Privacy & Security › Automation or › App Management; upup says which.
62
66
 
@@ -64,7 +68,7 @@ macOS may stop upup from quitting an app or replacing one until the app upup run
64
68
 
65
69
  An Audit asks a model whether an offered update is safe to take. upup downloads both versions from the package's registry (for npm, also each direct dependency whose declared version changed), verifies their checksums, extracts and diffs them, links each version to the source commit it was built from where it can (Sigstore-verified npm provenance, a matching GitHub release asset, or the commit the package declares, labelled by strength) and diffs that source too, and lets the model read that workspace through read, grep and list tools confined to it. A GitHub Action has no published package: upup fetches its repository with git at the commits the two tags point at and diffs those, committed bundles included. Tags can move, so an update is listed with the commit its tag points at: the Audit is of that commit and a pin is moved to exactly it, and once the tag points elsewhere upup won't audit it or move a pin to it until you refresh. A workflow that names a tag runs whatever the tag points at later. Changes in a file's mode, such as a script made executable, and the commits a submodule moved between are listed beside the diff; a submodule's own files are not fetched. An action whose repository is private, or which only the repository's own server has, is not audited. The model has no shell, network or write access. It answers with a verdict (`ok`, `caution`, `danger` or `no-verdict`), a rationale and findings.
66
70
 
67
- Choose the model and thinking level in Settings (`,`) from every model [pi-ai](https://github.com/earendil-works/pi) knows. upup shares pi's credentials file, `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`), so a provider you signed in to with pi works here, and keys or logins added in upup are visible to pi. With **audit first** on, `u` audits the selection and applies only the updates whose audit found `ok`. An update already audited as `caution`, `danger` or `no verdict` is marked in the confirmation instead, and confirming installs it anyway. Agents over MCP can still apply only `ok` updates, apart from OS and app updates.
71
+ Choose the model and thinking level in Settings (`,`) from every model [pi-ai](https://github.com/earendil-works/pi) knows. upup shares pi's credentials file, `~/.pi/agent/auth.json` (or `$PI_CODING_AGENT_DIR/auth.json`), so a provider you signed in to with pi works here, and keys or logins added in upup are visible to pi. With **audit first** on, `u` audits the selection and applies only the updates whose audit found `ok`. An update already audited as `caution`, `danger` or `no verdict` is marked in the confirmation instead, and confirming installs it anyway. Agents over MCP can still apply only `ok` updates, apart from OS and app updates and pinned Node.js versions.
68
72
 
69
73
  Audits run four at a time, are stored under your user cache directory (`$XDG_CACHE_HOME/upup` when set, otherwise `~/Library/Caches/upup` on macOS and `~/.cache/upup` elsewhere), and start over if upup quits mid-audit. A Report is reused while the update resolves to the same artifact digests and the model, the thinking level and the prompt are unchanged. It approves the package's own published files; changed dependencies are shown to the model but not pinned, because the package manager resolves them when it installs. Failed, interrupted, stale and replaced runs are kept under `audits/history/` in the same directory, so a bad run can be inspected afterwards. Downloaded and extracted packages and source checkouts are removed once an Audit is done; only the diff and its summaries stay, for 30 days.
70
74
 
@@ -76,40 +80,42 @@ Audits run four at a time, are stored under your user cache directory (`$XDG_CAC
76
80
  claude mcp add upup -- upup mcp
77
81
  ```
78
82
 
79
- Agents can't choose the audit model or add credentials; set those in the TUI. With audit first on, agents can apply only updates whose Audit found `ok`, except OS and app updates, which can't be audited and install as they do in the TUI. Agents can install OS updates, and app updates that need root, only where sudo needs no password, since only the TUI can ask for one. An app that is open when an agent updates it fails its update instead of being quit.
83
+ Agents can't choose the audit model or add credentials; set those in the TUI. With audit first on, agents can apply only updates whose Audit found `ok`, except OS and app updates and pinned Node.js versions, which can't be audited and install as they do in the TUI. Agents can install OS updates, and app updates that need root, only where sudo needs no password, since only the TUI can ask for one. An app that is open when an agent updates it fails its update instead of being quit.
80
84
 
81
85
  ## Keys
82
86
 
83
- | Key | Action |
84
- | -------------------------------- | ------------------------------------------------------------------------ |
85
- | `j`/`k`, ↑/↓, PgUp/PgDn, `g`/`G` | Move |
86
- | Space | Select the package under the cursor |
87
- | `*` | Select every package shown, or clear them |
88
- | `/` | Filter by name (Enter keeps, Esc clears); hidden selections still count |
89
- | Enter, Esc | Open and close details on narrow terminals |
90
- | `u` | Update the selection, or the package under the cursor |
91
- | `d` | Remove the selection, or the package under the cursor |
92
- | `r` | Audit the selection, or the package under the cursor |
93
- | `o` | Open the package's homepage, or where its update installs from |
94
- | Tab, Shift+Tab | Switch between About, Changes, Report and Transcript |
95
- | Ctrl+D/Ctrl+U | Scroll details |
96
- | Ctrl+X | Stop: targets not yet started are skipped; started edits still reconcile |
97
- | `m` | Switch between the system inventory and the project |
98
- | `R` | Discover and list again, and re-read stored Reports |
99
- | `,` | Settings: audit model, thinking level and audit first |
100
- | `?` | List the keys |
101
- | `q` | Quit, after stopping a running update |
102
- | Ctrl+C | Quit immediately |
87
+ | Key | Action |
88
+ | --- | --- |
89
+ | `j`/`k`, ↑/↓, PgUp/PgDn, `g`/`G` | Move |
90
+ | Space | Select the package under the cursor |
91
+ | `*` | Select every package shown, or clear them |
92
+ | `/` | Filter by name (Enter keeps, Esc clears); hidden selections still count |
93
+ | Enter, Esc | Open and close details on narrow terminals |
94
+ | `u` | Update the selection, or the package under the cursor |
95
+ | `d` | Remove the selection, or the package under the cursor |
96
+ | `r` | Audit the selection, or the package under the cursor |
97
+ | `o` | Open the package's homepage, or where its update installs from |
98
+ | Tab, Shift+Tab | Switch between About, Changes, Report and Transcript |
99
+ | Ctrl+D/Ctrl+U | Scroll details |
100
+ | Ctrl+X | Stop: targets not yet started are skipped; started edits still reconcile |
101
+ | `m` | Switch between the system inventory and the project |
102
+ | `R` | Discover and list again, and re-read stored Reports |
103
+ | `,` | Settings: audit model, thinking level and audit first |
104
+ | `?` | List the keys |
105
+ | `q` | Quit, after stopping a running update |
106
+ | Ctrl+C | Quit immediately |
103
107
 
104
108
  Details have four tabs. **About** shows what the package is: its provider, versions and declaration, plus a description, release dates, popularity, deprecations and links from its registry, cached for a day under your user cache directory. **Changes** shows the verified diff between the installed and offered versions without needing a model; for an app update, the release notes from the app's feed. **Report** and **Transcript** show the latest Audit's verdict and what the model did.
105
109
 
106
- When npm's allow-scripts policy blocks an update, upup asks before retrying: it names the blocked install scripts. For a global install, approving adds those packages to your allow list for that one command. For a project, npm accepts no allow list from the command line, so approving turns its strict check off for that one install, and any install script in the project's dependencies may run. If npm's strict check crashes instead (`ENOENT` on a `lib` folder, which happens with `vp add -g` and `strict-allow-scripts=true`), retrying turns the strict check off for that one command, and the popup says so.
110
+ When npm's allow-scripts policy blocks an update, upup asks before retrying: it names the blocked install scripts. For a global install, approving adds those packages to your allow list for that one command. For a project, npm accepts no allow list from the command line, so approving turns its strict check off for that one install, and any install script in the project's dependencies may run. If npm's strict check crashes instead (`ENOENT` on a `lib` folder, which happens with `vp add -g` and `strict-allow-scripts=true`), retrying turns the strict check off for that one command, and the popup says so. Saying no skips the update, unless upup was already installing a project's edits: those are written to its lockfile by then, so the update fails, saying npm blocked its install scripts.
107
111
 
108
112
  Each update run is also logged to `updates/` in the user cache directory, one JSON line per package with its outcome and, for a failure, the command's full output. The 50 newest runs are kept.
109
113
 
110
114
  ## Development
111
115
 
112
- Requires Node 26.4 or newer and [Vite+](https://viteplus.dev).
116
+ Requires Node 26.12 or newer and [Vite+](https://viteplus.dev).
117
+
118
+ Run `mise install` once to get the Go toolchain `mise.toml` pins; a binary-audit test reads its build info and is skipped without Go.
113
119
 
114
120
  ```sh
115
121
  vp install
@@ -118,6 +124,10 @@ vp test
118
124
  vp pack
119
125
  ```
120
126
 
121
- `vp run sea` builds a standalone `dist-sea/upup` for this machine (macOS or Linux), with Node and OpenTUI's native library inside, so it runs without Node installed. It extracts those assets to a per-user directory under the system temp directory on first run. `vp run sea:install` builds it and copies it to `~/.local/bin/upup`.
127
+ `vp check` formats, lints and type-checks with [Ultracite](https://www.ultracite.ai)'s presets and `@effect/tsgo`'s Effect diagnostics, configured in `oxlint.config.ts` and `oxfmt.config.ts`; `vp check --fix` applies the fixes, so review its diff. `vp run ready` runs the checks, the tests and the build, as CI does.
128
+
129
+ `vp run sparkle` builds upup-sparkle (`native/sparkle`) on macOS, with Xcode's command line tools, into `native/dist`, signed ad hoc unless `UPUP_SIGNING_IDENTITY` names a signing identity, and notarized when `UPUP_NOTARY_KEY` (an App Store Connect API key's .p8 file), `UPUP_NOTARY_KEY_ID` and `UPUP_NOTARY_ISSUER` are set; and then starts it as upup would; the release workflow signs and notarizes it with a Developer ID, and starts it only once its signing identity and notary key are gone. `vp run sparkle:e2e` builds it and installs updates of a fixture app through it, from an appcast served on this Mac.
130
+
131
+ `vp run sea` builds a standalone `dist-sea/upup` for this machine (macOS or Linux), with Node, OpenTUI's native library and the Audit's grep worker inside, and on macOS upup-sparkle from `native/dist`, built again first if its source changed since, so it runs without Node installed. It extracts those assets to a per-user directory under the system temp directory on first run. `vp run sea:install` builds it and copies it to `~/.local/bin/upup` as a new file, since macOS kills a running copy whose file is overwritten in place.
122
132
 
123
133
  The Go implementation lives in history at `d2a5ed7`.