pyselfupdate 0.5.2__tar.gz → 0.5.4__tar.gz
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.
- pyselfupdate-0.5.4/PKG-INFO +391 -0
- pyselfupdate-0.5.4/README.md +368 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/pyproject.toml +1 -1
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/__init__.py +1 -1
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/config.py +12 -4
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/github.py +18 -6
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/install.py +68 -7
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/typercmd.py +5 -5
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/updater.py +8 -2
- pyselfupdate-0.5.2/PKG-INFO +0 -311
- pyselfupdate-0.5.2/README.md +0 -288
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/.gitignore +0 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/LICENSE +0 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/errors.py +0 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/notifier.py +0 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/py.typed +0 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/source.py +0 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/state.py +0 -0
- {pyselfupdate-0.5.2 → pyselfupdate-0.5.4}/src/pyselfupdate/version.py +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyselfupdate
|
|
3
|
+
Version: 0.5.4
|
|
4
|
+
Summary: Self-update and update notification for Python CLIs installed with uv tool
|
|
5
|
+
Project-URL: Repository, https://github.com/datapointchris/pyselfupdate
|
|
6
|
+
Project-URL: Issues, https://github.com/datapointchris/pyselfupdate/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/datapointchris/pyselfupdate/releases
|
|
8
|
+
Author-email: Chris Birch <datapointchris@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cli,release,self-update,update-notifier,uv
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: System :: Software Distribution
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Provides-Extra: typer
|
|
21
|
+
Requires-Dist: typer>=0.12.0; extra == 'typer'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pyselfupdate
|
|
25
|
+
|
|
26
|
+
Self-update and update notification for Python CLIs installed with `uv tool`.
|
|
27
|
+
|
|
28
|
+
A CLI installed with `uv tool install` has no way to tell its user that a newer
|
|
29
|
+
release exists, so it silently drifts. This package adds two layers, used
|
|
30
|
+
independently. `notify` prints one line, at most once a day, when a newer
|
|
31
|
+
release exists. `update` installs that release when the user asks.
|
|
32
|
+
|
|
33
|
+
`uv tool upgrade` re-resolves the requirement a tool was installed with, so a
|
|
34
|
+
tool installed at a release tag stays at that tag. `update` asks the release
|
|
35
|
+
source which tag is newest and reinstalls at that tag. A git install is held to
|
|
36
|
+
that tag's `uv.lock`.
|
|
37
|
+
|
|
38
|
+
The package has no runtime dependencies. It uses `urllib` for the network,
|
|
39
|
+
`tomllib` for uv's receipt and lock, and its own semver implementation. CI
|
|
40
|
+
installs it alone into an empty virtual environment and imports every module
|
|
41
|
+
except the typer integration.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add pyselfupdate # the library alone, with no dependencies
|
|
47
|
+
uv add 'pyselfupdate[typer]' # adds typer and the ready-made `update` command
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Python 3.11 is the floor. It is the first release carrying `tomllib`, which
|
|
51
|
+
reads uv's receipt and lock without a TOML dependency.
|
|
52
|
+
|
|
53
|
+
## Starting from scratch
|
|
54
|
+
|
|
55
|
+
Wire both layers into a typer CLI. `tool` is the name `uv tool install` gives
|
|
56
|
+
the tool, and `owner` is the GitHub account its releases are published under.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import typer
|
|
60
|
+
|
|
61
|
+
from pyselfupdate import Config, notify
|
|
62
|
+
from pyselfupdate.typercmd import add_update_command
|
|
63
|
+
|
|
64
|
+
app = typer.Typer()
|
|
65
|
+
CONFIG = Config(tool='mytool', owner='you') # releases at github.com/you/mytool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.callback()
|
|
69
|
+
def main() -> None:
|
|
70
|
+
notify(CONFIG) # never raises; ignore what it returns
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
add_update_command(app, CONFIG) # registers `mytool update`
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then publish a release, install the tool at its tag, and ask it:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
gh release create v1.0.0 --generate-notes # a GitHub release, not a bare tag
|
|
80
|
+
uv tool install 'mytool @ git+https://github.com/you/mytool@v1.0.0' # at a tag, never a branch
|
|
81
|
+
mytool update --check # ✓ mytool already at latest: v1.0.0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The running version is read from the installed distribution's metadata, so a
|
|
85
|
+
release's tag has to match the version its package declares. A tool versioned
|
|
86
|
+
from its git tags gets that for free.
|
|
87
|
+
|
|
88
|
+
The install names a tag because an install from a branch, a commit or a local
|
|
89
|
+
checkout is never updated. Its version says nothing about how far behind it
|
|
90
|
+
is. [What may be updated](#what-may-be-updated-is-read-from-uvs-receipt-not-guessed-from-the-version)
|
|
91
|
+
lists every case.
|
|
92
|
+
|
|
93
|
+
A private repository needs a credential. `gh auth login` is enough, because
|
|
94
|
+
the check runs `gh auth token` by default.
|
|
95
|
+
|
|
96
|
+
**When the notice never appears, run `mytool update --check` first.** It runs
|
|
97
|
+
the two steps that can fail, reading uv's receipt and asking the release
|
|
98
|
+
source, and prints what stopped it. The notice stays silent on both. Every
|
|
99
|
+
other reason it stays silent is in the table under `notify`.
|
|
100
|
+
|
|
101
|
+
## `notify` — one line after the command's output, never an error
|
|
102
|
+
|
|
103
|
+
Once per 24 hours, when a newer release exists, one line goes to stderr:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
mytool v1.4.0 available (running v1.3.2) — run `mytool update`
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
By default the line is printed at interpreter exit, so it lands after the
|
|
110
|
+
command's own output rather than inside it.
|
|
111
|
+
|
|
112
|
+
`notify` never raises, never installs anything, and never prints an error. A
|
|
113
|
+
check that fails is recorded in the state file and swallowed, because an update
|
|
114
|
+
notice must not be able to break the command the user typed.
|
|
115
|
+
|
|
116
|
+
Nothing is printed when any of these hold:
|
|
117
|
+
|
|
118
|
+
| Condition | Why |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `NO_AUTO_UPDATE` or `MYTOOL_NO_AUTO_UPDATE` is set | Opted out |
|
|
121
|
+
| `--help` or `-h` comes before any `--` on the command line | typer runs the callback even for a subcommand's help screen, which is not work |
|
|
122
|
+
| stdout or stderr is not a terminal | `mytool list > out 2>&1` must stay clean |
|
|
123
|
+
| `CI`, `BUILD_NUMBER`, `RUN_ID`, `GITHUB_ACTIONS` or `CODESPACES` is set | Not a human |
|
|
124
|
+
| Installed from a local path, an editable checkout, a branch or a commit | Nothing to compare against |
|
|
125
|
+
| Checked within the interval | One request per interval, not per invocation |
|
|
126
|
+
|
|
127
|
+
The opt-out is presence-only, so `NO_AUTO_UPDATE=0` disables the notice, the
|
|
128
|
+
same way [`NO_COLOR`](https://no-color.org) works. The interval has its own
|
|
129
|
+
pair, `MYTOOL_AUTO_UPDATE_INTERVAL` falling back to `AUTO_UPDATE_INTERVAL`.
|
|
130
|
+
Each takes `30m`, `6h`, `7d` or a bare number of seconds. The tool's form of
|
|
131
|
+
each variable is its name uppercased, with `-` written as `_`.
|
|
132
|
+
|
|
133
|
+
A command writing into a pager, a log or a structured-output mode calls
|
|
134
|
+
`notify(config, interactive=False)`. Nothing about the streams reveals that.
|
|
135
|
+
|
|
136
|
+
## `update` — rebuilds the environment it runs in, so it runs last
|
|
137
|
+
|
|
138
|
+
`update` runs `uv tool install --force`, which rebuilds the virtual environment
|
|
139
|
+
the running interpreter lives in. Replacing a Unix binary leaves a running
|
|
140
|
+
process untouched, because the process holds an inode. Rebuilding a venv pulls
|
|
141
|
+
modules out from under a live interpreter, so anything imported afterwards can
|
|
142
|
+
fail in ways that are hard to read.
|
|
143
|
+
|
|
144
|
+
So make `update` the last thing the process does, then call `exit_now`. Or call
|
|
145
|
+
`update_and_reexec`, which replaces the process with the release it installed,
|
|
146
|
+
running the same command line. Its caller never sees the `Result`, so it writes
|
|
147
|
+
`lock_warning` to stderr itself before the re-exec.
|
|
148
|
+
|
|
149
|
+
`check` reports what `update` would install, and installs nothing:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from pyselfupdate import Config, check, require_updatable
|
|
153
|
+
|
|
154
|
+
config = Config(tool='mytool', owner='you')
|
|
155
|
+
|
|
156
|
+
require_updatable(config) # reads uv's receipt; raises on an install update refuses
|
|
157
|
+
result = check(config) # one request to the release source
|
|
158
|
+
if result.update_available:
|
|
159
|
+
print(result.current, '->', result.latest)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`check` compares the running version with the newest release, and reads
|
|
163
|
+
nothing else. A branch build's version can rank below the release it is ahead
|
|
164
|
+
of. Three commits past `v1.2.3`, a tool versioned from git tags builds as
|
|
165
|
+
`1.2.3-post.3+<commit>`, which semver ranks below `v1.2.3`. So call
|
|
166
|
+
`require_updatable` before reporting what `check` found. It refuses every
|
|
167
|
+
install `update` refuses.
|
|
168
|
+
|
|
169
|
+
The typer extra carries the whole command. `add_update_command(app, config)`
|
|
170
|
+
registers `mytool update`. `run_update(config, check_only=...)` is the same
|
|
171
|
+
reporting, for a CLI that declares the command itself. Both read the receipt
|
|
172
|
+
before the network, so `mytool update --check` refuses the same installs
|
|
173
|
+
`update` does, printing the refusal and exiting 1. Both fetch the changelog
|
|
174
|
+
before the install, because nothing can be imported after it.
|
|
175
|
+
|
|
176
|
+
A caller that needs its own steps between the check and the install composes
|
|
177
|
+
the three pieces `update` is made of:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import sys
|
|
181
|
+
|
|
182
|
+
from pyselfupdate import Config, changelog, check, exit_now, install_release, require_updatable
|
|
183
|
+
|
|
184
|
+
config = Config(tool='mytool', owner='you')
|
|
185
|
+
|
|
186
|
+
installation = require_updatable(config) # refuses before any request
|
|
187
|
+
result = check(config) # the network, while the environment is intact
|
|
188
|
+
notes = changelog(config, result.current, result.latest) # fetched before the install, printed after
|
|
189
|
+
installed = install_release(config, result, installation) # a no-op when nothing is newer
|
|
190
|
+
print('\n'.join(notes))
|
|
191
|
+
if installed.lock_warning:
|
|
192
|
+
print(installed.lock_warning, file=sys.stderr)
|
|
193
|
+
exit_now() # flushes, then exits without unwinding through code that may import
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Every error is a `SelfUpdateError`, and each cause has its own subclass, so a
|
|
197
|
+
caller matches a type rather than message text:
|
|
198
|
+
|
|
199
|
+
| Error | Raised when |
|
|
200
|
+
| --- | --- |
|
|
201
|
+
| `InvalidConfigError` | `tool` is empty, or `owner` is empty without a custom `source` |
|
|
202
|
+
| `LocalInstallError` | The install is a local checkout, a branch or a commit, or the running version is not a semantic version |
|
|
203
|
+
| `NotInstalledError` | uv's receipt is missing or unreadable, or holds a `--with` requirement an update would drop |
|
|
204
|
+
| `NoReleaseError` | The source publishes no release, or its newest tag is not a semantic version |
|
|
205
|
+
| `SourceError` | The source could not be reached, or answered with something unusable |
|
|
206
|
+
| `LockUnreadableError` | The release's `uv.lock` could not be read, so nothing was installed |
|
|
207
|
+
| `InstallFailedError` | `uv tool install` ran and failed, or uv is not on `PATH` |
|
|
208
|
+
|
|
209
|
+
### A git install is held to the release's `uv.lock`
|
|
210
|
+
|
|
211
|
+
`uv tool install` never reads a lock. Handed a git requirement, it resolves
|
|
212
|
+
every dependency to its newest version, so a tool would run on a set its CI
|
|
213
|
+
never tested. `update` therefore clones the release's tag and exports its
|
|
214
|
+
`uv.lock`. It passes registry pins as `--constraints`, and URL pins, such as a
|
|
215
|
+
git dependency, as `--overrides`. uv records both in the tool's receipt, so a
|
|
216
|
+
later `uv tool upgrade` stays held to them.
|
|
217
|
+
|
|
218
|
+
| The tag | What `update` does |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| Carries a `uv.lock` | Installs the locked versions |
|
|
221
|
+
| Carries none | Installs unlocked and sets `lock_missing`. `lock_warning` names the tag, and `run_update` and `update_and_reexec` print it |
|
|
222
|
+
| Will not clone, uv will not export its lock, or git or uv is not on `PATH` | Raises `LockUnreadableError` and installs nothing |
|
|
223
|
+
|
|
224
|
+
An install from an index reads no lock, because a wheel carries none. It
|
|
225
|
+
takes the release as a floor, `mytool>=2.0.0`, rather than a pin. uv records
|
|
226
|
+
the specifier in the receipt, and `uv tool upgrade` refuses to move an exact
|
|
227
|
+
pin.
|
|
228
|
+
|
|
229
|
+
An update reinstalls the tool's own extras, as in `mytool[fast]`, with their
|
|
230
|
+
dependencies held to the lock. Every `--with` and `--with-editable`
|
|
231
|
+
requirement is passed back to uv as well.
|
|
232
|
+
|
|
233
|
+
`uv tool install --force` reads nothing back from the receipt it replaces. So
|
|
234
|
+
an update passes back the receipt's `--python` and the flags naming where its
|
|
235
|
+
packages come from: `--index`, `--default-index`, `--index-url`,
|
|
236
|
+
`--extra-index-url`, `--find-links`, `--no-index`, `--index-strategy` and
|
|
237
|
+
`--keyring-provider`. Without them, a tool installed with
|
|
238
|
+
`--index https://pypi.example.com/simple/` resolves its update against PyPI
|
|
239
|
+
alone and is not found. An `explicit` index is left out, because only a config
|
|
240
|
+
file can declare one and the reinstall reads that file again. Dropping any
|
|
241
|
+
other recorded option changes how the tool resolves or builds, never whether it
|
|
242
|
+
is found.
|
|
243
|
+
|
|
244
|
+
## What may be updated is read from uv's receipt, not guessed from the version
|
|
245
|
+
|
|
246
|
+
uv writes `uv-receipt.toml` into the tool's directory at install time. It
|
|
247
|
+
records where the tool came from, which the running program cannot know:
|
|
248
|
+
|
|
249
|
+
| Receipt | Result |
|
|
250
|
+
| --- | --- |
|
|
251
|
+
| `git = "...git?rev=v1.2.3"` | Updatable |
|
|
252
|
+
| `name = "mytool"` (from an index) | Updatable |
|
|
253
|
+
| `git = "...git"` with no `rev` | Refused: it tracks the default branch, so its version says nothing about how far behind it is |
|
|
254
|
+
| `git = "...git?rev=main"`, or a `rev` naming a commit | Refused: uv writes `rev=` for whatever followed the `@`, so a `rev` counts as a release only when it is `tag_prefix` followed by a semantic version |
|
|
255
|
+
| `directory`, `path` or `editable` | Refused: reinstalling would discard a working copy |
|
|
256
|
+
| A `--with` requirement no requirements-file line reproduces, such as a relative path | Refused with `NotInstalledError`: the update would drop it |
|
|
257
|
+
|
|
258
|
+
`update` and `mytool update --check` refuse every row marked Refused. The
|
|
259
|
+
notice stays silent on each of them except the `--with` row, because a newer
|
|
260
|
+
release is still worth knowing about there.
|
|
261
|
+
|
|
262
|
+
A tool whose receipt is missing or unreadable fails closed. The notice treats
|
|
263
|
+
it as local, and `update` raises `NotInstalledError`.
|
|
264
|
+
|
|
265
|
+
A caller with a custom `source` whose refs carry a prefix sets `tag_prefix` to
|
|
266
|
+
match. Without it, a tag install from that source is refused as a branch
|
|
267
|
+
install.
|
|
268
|
+
|
|
269
|
+
## `Config` — the tool's name, and every default derived from it
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from pyselfupdate import Config
|
|
273
|
+
|
|
274
|
+
config = Config(
|
|
275
|
+
tool='mytool', # required: the uv tool name, the state directory, the variable prefix
|
|
276
|
+
owner='you', # GitHub owner; required unless source is given
|
|
277
|
+
repo='mytool', # defaults to tool
|
|
278
|
+
package='mytool', # the distribution the version is read from; defaults to tool
|
|
279
|
+
version='1.2.3', # defaults to that distribution's installed metadata
|
|
280
|
+
token='', # see Authentication below; you almost certainly want the default
|
|
281
|
+
token_func=None, # a source of your own, tried before $GITHUB_TOKEN_COMMAND
|
|
282
|
+
timeout=10.0, # seconds, per request
|
|
283
|
+
tag_prefix='', # 'cli/' for tags like cli/v1.2.3
|
|
284
|
+
allow_prerelease=False,
|
|
285
|
+
source=None, # a custom Source; defaults to a GitHubSource built from the fields above
|
|
286
|
+
)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`source` is the seam for releases published somewhere other than GitHub. A
|
|
290
|
+
`Source` is anything with `latest_release()` returning a `Release`. A source
|
|
291
|
+
that also has `changelog(from_ref, to_ref)`, returning commit subjects, gets a
|
|
292
|
+
changelog in `mytool update`. One without it gets none, and that is not an
|
|
293
|
+
error.
|
|
294
|
+
|
|
295
|
+
## Authentication — on by default, through `gh auth token`
|
|
296
|
+
|
|
297
|
+
**Configure nothing.** `GitHubSource` runs `gh auth token` when a request is
|
|
298
|
+
about to be made, and sends what it prints.
|
|
299
|
+
|
|
300
|
+
The alternative is not "no credential". It is 60 requests an hour, charged
|
|
301
|
+
**per IP address** and shared with every other anonymous caller behind the same
|
|
302
|
+
egress. A default that has to be opted into is a default nobody sets.
|
|
303
|
+
|
|
304
|
+
Four sources, first non-empty wins:
|
|
305
|
+
|
|
306
|
+
| Source | Set by | Default |
|
|
307
|
+
| --- | --- | --- |
|
|
308
|
+
| `Config.token` | you, in code | unset |
|
|
309
|
+
| `$GITHUB_TOKEN`, then `$GH_TOKEN` | whoever runs your CLI | unset |
|
|
310
|
+
| `token_func()` | you, in code | unset |
|
|
311
|
+
| `$GITHUB_TOKEN_COMMAND` | whoever runs your CLI | `gh auth token` |
|
|
312
|
+
|
|
313
|
+
`$GITHUB_TOKEN_COMMAND` both redirects and disables, which is what a switch has
|
|
314
|
+
to do to be worth having:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
GITHUB_TOKEN_COMMAND='pass show github/token' # use this instead
|
|
318
|
+
GITHUB_TOKEN_COMMAND='op read op://vault/gh/token'
|
|
319
|
+
GITHUB_TOKEN_COMMAND='' # run nothing, stay anonymous
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
It never raises. A command that is not installed, exits non-zero, or takes
|
|
323
|
+
longer than ten seconds degrades to an unauthenticated request, which still
|
|
324
|
+
works against a public repository.
|
|
325
|
+
|
|
326
|
+
`token_func` is for a credential neither the environment nor a command can
|
|
327
|
+
produce. It is called lazily, for the same reason the command is. The notify
|
|
328
|
+
gate resolves a `Config` on every invocation and declines most of them without
|
|
329
|
+
reaching the network, and a subprocess in front of that gate is the entire
|
|
330
|
+
cost worth avoiding.
|
|
331
|
+
|
|
332
|
+
**The variable names and the default command belong to `GitHubSource`, not to
|
|
333
|
+
`Config`.** A credential is the host's business. A `Source` for another code
|
|
334
|
+
host brings its own variable and its own command, and nothing above the
|
|
335
|
+
`Source` protocol learns either name.
|
|
336
|
+
|
|
337
|
+
## State — one file per tool per machine, stamped before the network call
|
|
338
|
+
|
|
339
|
+
`${XDG_STATE_HOME:-~/.local/state}/<tool>/autoupdate-<machine>.json`, written
|
|
340
|
+
atomically:
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{
|
|
344
|
+
"tool": "mytool",
|
|
345
|
+
"schema": 1,
|
|
346
|
+
"checked_at": "2026-07-26T15:07:15Z",
|
|
347
|
+
"checked_at_epoch": 1785078435,
|
|
348
|
+
"current_version": "v1.3.2",
|
|
349
|
+
"latest_version": "v1.4.0",
|
|
350
|
+
"last_error": ""
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
State, not config and not cache: it persists across runs, it is not authored by
|
|
355
|
+
the user, and deleting it changes behavior rather than merely costing a
|
|
356
|
+
recompute. That is `XDG_STATE_HOME` by the Base Directory specification, and it
|
|
357
|
+
is where `gh` puts the same thing.
|
|
358
|
+
|
|
359
|
+
`<machine>` is the bare hostname, domain dropped and lowercased, from
|
|
360
|
+
`pyselfupdate.state.machine()`. It is `unknown` when the hostname cannot be
|
|
361
|
+
read. It is in the name because both fields that vary describe one machine:
|
|
362
|
+
the version installed here, and the instant this box last checked. A state
|
|
363
|
+
directory shared between machines, by a file syncer or a network home
|
|
364
|
+
directory, otherwise has two writers on one path. It reports whichever wrote
|
|
365
|
+
last as the state of all of them.
|
|
366
|
+
|
|
367
|
+
The timestamp is written **before** the network call. `gh` stamps only on
|
|
368
|
+
success, so a rate-limited or offline user re-hits the API on every invocation
|
|
369
|
+
until the window resets. An interval exists to bound the request rate, and only
|
|
370
|
+
this ordering does that.
|
|
371
|
+
|
|
372
|
+
A gate that declines to check writes nothing, so a missing file means the
|
|
373
|
+
network was never reached. A check that failed leaves its error in
|
|
374
|
+
`last_error`. `read_state('mytool')` reads the file back.
|
|
375
|
+
|
|
376
|
+
## Siblings — the same two layers for a Go binary and a git checkout
|
|
377
|
+
|
|
378
|
+
All three carry both layers. They share the `autoupdate-<machine>.json` schema,
|
|
379
|
+
the machine derivation that names it, and the `NO_AUTO_UPDATE` and
|
|
380
|
+
`AUTO_UPDATE_INTERVAL` variables. They do not share an API, because "update"
|
|
381
|
+
means a different operation in each.
|
|
382
|
+
|
|
383
|
+
- [goselfupdate](https://github.com/datapointchris/goselfupdate) — replaces a Go binary
|
|
384
|
+
- [bashselfupdate](https://github.com/datapointchris/bashselfupdate) — moves a git checkout to its newest tag
|
|
385
|
+
|
|
386
|
+
Version precedence is deliberately identical across all three, so a tool and
|
|
387
|
+
its siblings never disagree about which release is newer.
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
MIT
|