cdx-manager 0.25.4 → 0.25.6
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 +1 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_25_5.md +14 -0
- package/changelogs/CHANGELOGS_0_25_6.md +15 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +8 -0
- package/checksums/release-archives.json +22 -0
- package/docs/cli.md +53 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/commands/maintenance.py +8 -2
- package/src/update_all.py +115 -53
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.25.
|
|
1
|
+
0.25.6
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CDX Manager 0.25.5
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-02
|
|
4
|
+
|
|
5
|
+
This patch release makes `cdx update all` report version status based on verified evidence.
|
|
6
|
+
|
|
7
|
+
## Fixes
|
|
8
|
+
|
|
9
|
+
- Compare installed tool versions with official release sources before reporting them as up to date. Report unknown status when version evidence is unavailable or inconsistent, and offer automatic updates only when a supported CLI updater or verified Homebrew installation is available. Preserve existing JSON fields and expose uncertainty with `unknown` and an optional `reason`. (#85)
|
|
10
|
+
|
|
11
|
+
## Maintenance
|
|
12
|
+
|
|
13
|
+
- Record tray companion checksums for v0.25.4. (`46ff59ba5fad3bbd7b37429371112f3f6f38a330`)
|
|
14
|
+
- Record release archive checksums for v0.25.4. (`8ae658e2e6f3303f575e95f58b622321cad87a60`)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# CDX Manager 0.25.6
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-02
|
|
4
|
+
|
|
5
|
+
This patch release fixes provider update detection and verifies installed versions after updates.
|
|
6
|
+
|
|
7
|
+
## Fixes
|
|
8
|
+
|
|
9
|
+
- Detect advertised provider updaters and version output on both stdout and stderr. After an update, verify the installed version and report failure if it is missing, unchanged, downgraded, or still below the published version. Successful text output shows the version transition, and JSON results include before, after, and latest versions. (#87)
|
|
10
|
+
- Fall back to the latest stable GitHub release when Ollama's vendor API returns an empty, invalid, or failed response. Keep Linux Ollama updates manual, with an explicit maintenance reason. (#87)
|
|
11
|
+
|
|
12
|
+
## Maintenance
|
|
13
|
+
|
|
14
|
+
- Record tray companion checksums for v0.25.5. (`b64a372f9ab4abb61f1440943fca19703ad555eb`)
|
|
15
|
+
- Record release archive checksums for v0.25.5. (`1218145eae4201e40b8140e7a94cbcec18454e17`)
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
## Truthful workstation updates
|
|
4
|
+
|
|
5
|
+
- Detect native updater subcommands in both stdout and stderr, including Antigravity's indented `Available subcommands:` help. Verify the installed CLI version after updating, fail unchanged or unverified updates, and expose before/after versions without hiding major upgrades.
|
|
6
|
+
- Fall back from empty or failed Ollama vendor responses (including Linux HTTP 204) to the latest stable `ollama/ollama` GitHub release. Keep Linux updates manual with `update_available`, a null command, and a service-safety reason; never replace a potentially active binary or restart models automatically.
|
|
7
|
+
|
|
8
|
+
- Verify latest versions from npm, RTK GitHub releases, and Antigravity/Ollama vendor update endpoints with short read-only probes. Report `unknown` on unavailable evidence; reserve `up_to_date` for identical known versions, including Ponytail revisions.
|
|
9
|
+
- Preserve the existing JSON fields and `update_available` status, show uncertainty and manual-update reasons in text, and apply only supported native commands or verified Homebrew package upgrades. Cover matching/outdated versions, source failures, missing tools, and command execution with faked sources.
|
|
10
|
+
|
|
3
11
|
## Dependency security
|
|
4
12
|
|
|
5
13
|
- Require cryptography >=50.0.0 for encrypted auth bundles and remove the vulnerable 47.0.0 lock entry. Python 3.9 remains supported from 3.9.2, as cryptography 50 excludes 3.9.0 and 3.9.1.
|
|
@@ -727,6 +727,28 @@
|
|
|
727
727
|
"x86_64-pc-windows-msvc": "c81623ffb94b11ae7b7b28be123b49393240bb0edd31c69969ca07d7eab8dff7",
|
|
728
728
|
"x86_64-unknown-linux-musl": "6892112587c1d8c6e805a90148ec8deb6c04b5927c7ed0c18f19479d521c0ced"
|
|
729
729
|
}
|
|
730
|
+
},
|
|
731
|
+
"v0.25.4": {
|
|
732
|
+
"github_tarball_sha256": "94dbd4f9e2d7e34cf3cfaf967565c1fe8dd56a0cd1fe2888c43cf8f44290a2ec",
|
|
733
|
+
"github_zip_sha256": "13c3bad9b45776fb13a25bf8a363b98d5e8a042b91f848005e7f4adbfb275659",
|
|
734
|
+
"tray_assets": {
|
|
735
|
+
"aarch64-apple-darwin": "06b4b649621b53af642b01ba543369e7ce975454f3b70977e5d35c7a5d3bca5d",
|
|
736
|
+
"aarch64-unknown-linux-musl": "f5ced5b655c525bbcd71eeab002ea557e52507dbe69101ad351fea0bbbf290ff",
|
|
737
|
+
"x86_64-apple-darwin": "2ee4a2144455fc6eebd395c9c58c3d3fa7e4ed333e5f3959ba78e46a41c55be0",
|
|
738
|
+
"x86_64-pc-windows-msvc": "8e2c1925f025a7227ef909f27e8b4696c03bb5a86e0c61c989da6a942b5638a6",
|
|
739
|
+
"x86_64-unknown-linux-musl": "9ae1164abc3ff4b30d25c44696569855138f8ac0e3b6e1c3451c716d20f221fa"
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
"v0.25.5": {
|
|
743
|
+
"github_tarball_sha256": "4a2ece21e32f59a84d8bc7b2a565817c6c90bb314f179319bc19cf85a0c71a60",
|
|
744
|
+
"github_zip_sha256": "959de4dca501fd53c937f4045936b12acbea58d255a4e3cae510ec82a230bef8",
|
|
745
|
+
"tray_assets": {
|
|
746
|
+
"aarch64-apple-darwin": "727c4ded1ee4d9d8ed9853badcb10efd1cc76bd6dcd6012b6739920d42e6273c",
|
|
747
|
+
"aarch64-unknown-linux-musl": "9270224db6be83d4a4787f9b523a6fa059267f56ed1cf12f022f65bf68c0474f",
|
|
748
|
+
"x86_64-apple-darwin": "10c555671cdeb3ce4c0372f6f8a48e6cfeac9132e980c466fe5a8978dc1a2862",
|
|
749
|
+
"x86_64-pc-windows-msvc": "85945b30580cb82c64ece94652ae0633a6156dfd85e02108c0d4b7676cab9911",
|
|
750
|
+
"x86_64-unknown-linux-musl": "7c5a23938b84c5952be71b5956ed3167c218d05ce661940b0c86f2dde2d8221f"
|
|
751
|
+
}
|
|
730
752
|
}
|
|
731
753
|
}
|
|
732
754
|
}
|
package/docs/cli.md
CHANGED
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
| `cdx repair [--dry-run] [--force] [--json]` | Plan or apply safe repairs for missing state files, quarantines, and orphan profiles |
|
|
52
52
|
| `cdx view [--json] [--lan] [--lan-rw] [--focus <ref>] [--read] [--port <port>] [--host <host>] [--refresh-interval <s>] [--tls] [--tls-cert <path>] [--tls-key <path>] [--open] [--no-open]` | Open the Logics browser/focus viewer by delegating to `logics-manager view`; all viewer flags are forwarded; JSON mode reports diagnostics without launching it |
|
|
53
53
|
| `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
|
|
54
|
-
| `cdx update all [--yes] [--json]` | Check installed providers, RTK, and Ponytail across Codex profiles; show the plan and ask once before applying
|
|
54
|
+
| `cdx update all [--yes] [--json]` | Check installed providers, RTK, and Ponytail across Codex profiles; show the plan and ask once before applying supported native/Homebrew updates and missing RTK/Ponytail setup |
|
|
55
55
|
| `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
|
|
56
56
|
| `cdx notify` | Internal provider hook target for agent alerts and agent state; use `cdx set <name> --notify on` or `--agent-state on` to enable it |
|
|
57
57
|
| `cdx agents [--max-age-hours N] [--json]` | Each session's last known agent state (working, waiting, done, failed, limit, ended), waiting first; records older than N hours (default 24) are dropped. See [Agent state](sessions-and-notifications.md#agent-state) |
|
|
@@ -545,3 +545,55 @@ backup, and atomically replaces history only when the complete batch has no
|
|
|
545
545
|
conflict. Unrelated fields, usage, and malformed/unrelated history lines survive.
|
|
546
546
|
Repeating the same mapping is a no-op and creates no additional backup. The
|
|
547
547
|
registry and provider transcripts are never rewritten by backfill.
|
|
548
|
+
|
|
549
|
+
### Workstation update checks
|
|
550
|
+
|
|
551
|
+
`cdx update all --json` collects a read-only plan. `--yes` applies its supported
|
|
552
|
+
update and session setup steps. Existing `plan.items` fields remain available:
|
|
553
|
+
`installed`, `version`, `latest_version`, `update_command`, and `status`.
|
|
554
|
+
Versions and commands are explicitly `null` when unavailable.
|
|
555
|
+
|
|
556
|
+
- `up_to_date`: both versions are known and identical.
|
|
557
|
+
- `update_available`: the published version is newer (the tool is outdated).
|
|
558
|
+
- `unknown`: the source failed, returned invalid data, timed out, or the installed
|
|
559
|
+
version could not be compared reliably. This never means up to date.
|
|
560
|
+
- `not_installed`: the executable is absent; no installation is guessed.
|
|
561
|
+
|
|
562
|
+
Checks use the npm registry for Codex and `@anthropic-ai/claude-code`, GitHub's
|
|
563
|
+
`rtk-ai/rtk` latest stable release, the original Antigravity editor's vendor
|
|
564
|
+
`/releases` feed for `agy`, and Ollama's own `/api/update` endpoint with the local
|
|
565
|
+
OS/architecture and baseline version `0.0.0`. Ollama's local `/api/version` reports
|
|
566
|
+
an installed server, not the latest published release, and is not used.
|
|
567
|
+
Antigravity Hub and IDE 2 have separate feeds and are not substituted for `agy`.
|
|
568
|
+
HTTP and read-only CLI probes have a five-second timeout; source failures leave
|
|
569
|
+
`latest_version: null` and `status: unknown`. Ponytail also reports `unknown`
|
|
570
|
+
when its remote revision is unavailable. An empty, failed, or invalid Ollama
|
|
571
|
+
response (including Linux HTTP 204) falls back to the latest stable GitHub
|
|
572
|
+
release for `ollama/ollama`. If that source also fails, status is `unknown`;
|
|
573
|
+
a different platform's vendor response is never substituted.
|
|
574
|
+
|
|
575
|
+
For an outdated tool, CDX uses a native update command advertised by its
|
|
576
|
+
`--help` on either stdout or stderr (including Claude, Antigravity, and RTK
|
|
577
|
+
when available), Codex's updater,
|
|
578
|
+
or Homebrew only when the resolved executable belongs to a Caskroom/Cellar
|
|
579
|
+
package. An outdated tool without such a path retains `update_available`,
|
|
580
|
+
`update_command: null`, and a human-readable `reason`; it requires the vendor's
|
|
581
|
+
manual update mechanism. On Linux, Ollama always retains `update_available`
|
|
582
|
+
with `update_command: null` and an explicit maintenance reason: its installer
|
|
583
|
+
writes system paths and may restart a service. A binary in `~/.local/bin` can
|
|
584
|
+
also serve running models, so CDX neither replaces it nor restarts services.
|
|
585
|
+
Arrange a manual maintenance window. Source uncertainty never schedules a guessed install.
|
|
586
|
+
The text table shows `UNKNOWN` and the reason, and an empty action list makes
|
|
587
|
+
no claim that all tools are current. JSON consumers, including external
|
|
588
|
+
remote-ops auto-update scripts, should inspect statuses separately from successful
|
|
589
|
+
command execution: exit code zero means no attempted action failed, not that
|
|
590
|
+
every installed tool was verified current. With `--yes`, the returned plan is the
|
|
591
|
+
pre-update inventory; `results` records attempted actions and their exit codes.
|
|
592
|
+
|
|
593
|
+
After each successful tool update command, CDX probes the CLI version again.
|
|
594
|
+
An unchanged, unreadable, downgraded, or still older-than-published version
|
|
595
|
+
makes the action fail, even if the updater exited zero. Successful text output
|
|
596
|
+
shows the before/after versions, including major upgrades. JSON keeps the
|
|
597
|
+
pre-update plan and existing result fields; tool verification adds
|
|
598
|
+
`version_before`, `version_after`, and `latest_version` to successful-command
|
|
599
|
+
results, plus `reason` and a nonzero `returncode` if verification fails.
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -28,6 +28,7 @@ from ..update_manager import build_update_plan, format_update_failure, run_updat
|
|
|
28
28
|
def _format_update_all(plan, use_color=False):
|
|
29
29
|
labels = {
|
|
30
30
|
"up_to_date": _success("OK", use_color),
|
|
31
|
+
"unknown": _warn("UNKNOWN", use_color),
|
|
31
32
|
"update_available": _warn("UPDATE", use_color),
|
|
32
33
|
"not_installed": _warn("MISSING", use_color),
|
|
33
34
|
"check_with_native_updater": _info("CHECK", use_color),
|
|
@@ -36,6 +37,7 @@ def _format_update_all(plan, use_color=False):
|
|
|
36
37
|
for item in plan["items"]:
|
|
37
38
|
rows.append([_info(item["name"], use_color), item.get("version") or _dim("-", use_color), item.get("latest_version") or _dim("-", use_color), labels.get(item["status"], item["status"].replace("_", " "))])
|
|
38
39
|
lines = [_style("Workstation update", "1", use_color), _dim("Inventory only — nothing has changed.", use_color), "", _pad_table(rows)]
|
|
40
|
+
lines.extend(f"{item['name']}: {item['reason']}" for item in plan["items"] if item.get("reason"))
|
|
39
41
|
missing_rtk = plan["setup"]["rtk_missing_sessions"]
|
|
40
42
|
setup = [f"RTK: enable for {', '.join(missing_rtk)}" if missing_rtk else "RTK: configured for every session"]
|
|
41
43
|
setup.extend(f"Ponytail {plugin['session']}: {labels.get(plugin['status'], plugin['status'].replace('_', ' '))}" for plugin in plan["setup"]["ponytail"])
|
|
@@ -43,14 +45,15 @@ def _format_update_all(plan, use_color=False):
|
|
|
43
45
|
if plan["steps"]:
|
|
44
46
|
lines.extend(["", _warn(f"{len(plan['steps'])} action(s) ready", use_color), _dim("Confirm to update only the tools listed above.", use_color)])
|
|
45
47
|
else:
|
|
46
|
-
lines.extend(["",
|
|
48
|
+
lines.extend(["", _dim("No automatic actions available. See tool statuses and reasons above.", use_color)])
|
|
47
49
|
return "\n".join(lines)
|
|
48
50
|
|
|
49
51
|
def _format_update_all_result(result, use_color=False):
|
|
50
52
|
if result.get("skipped"):
|
|
51
53
|
return _warn(f"skipped: {result['name']} (blocked by {result['blocked_by']})", use_color)
|
|
52
54
|
if result.get("returncode") == 0:
|
|
53
|
-
|
|
55
|
+
versions = f" ({result['version_before']} -> {result['version_after']})" if "version_after" in result else ""
|
|
56
|
+
return _success(f"done: {result['name']}{versions}", use_color)
|
|
54
57
|
detail = str(result.get("stderr") or result.get("stdout") or "No error output.").strip().replace("\n", " ")
|
|
55
58
|
detail = detail[-500:]
|
|
56
59
|
command = shlex.join(result.get("command") or [])
|
|
@@ -689,6 +692,9 @@ def handle_update(rest, ctx):
|
|
|
689
692
|
if input("Apply this plan? [y/N] ").strip().lower() not in ("y", "yes"):
|
|
690
693
|
ctx["out"](f"{_warn('Cancelled.', ctx['use_color'])}\n")
|
|
691
694
|
return 0
|
|
695
|
+
if parsed["yes"] and not parsed["json"]:
|
|
696
|
+
ctx["out"](f"{_format_update_all(plan, ctx['use_color'])}\n")
|
|
697
|
+
|
|
692
698
|
def progress(event):
|
|
693
699
|
if parsed["json"]:
|
|
694
700
|
return
|
package/src/update_all.py
CHANGED
|
@@ -2,19 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import platform
|
|
5
6
|
import re
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
9
|
+
from urllib.parse import urlencode, urlparse
|
|
10
|
+
from urllib.request import Request, urlopen
|
|
8
11
|
|
|
9
12
|
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
|
|
10
13
|
|
|
11
14
|
_TOOLS = (
|
|
12
|
-
(PROVIDER_CODEX, "codex"
|
|
13
|
-
(PROVIDER_CLAUDE, "claude"
|
|
14
|
-
(PROVIDER_ANTIGRAVITY, "agy"
|
|
15
|
-
(PROVIDER_OLLAMA, "ollama"
|
|
16
|
-
("rtk", "rtk"
|
|
15
|
+
(PROVIDER_CODEX, "codex"),
|
|
16
|
+
(PROVIDER_CLAUDE, "claude"),
|
|
17
|
+
(PROVIDER_ANTIGRAVITY, "agy"),
|
|
18
|
+
(PROVIDER_OLLAMA, "ollama"),
|
|
19
|
+
("rtk", "rtk"),
|
|
17
20
|
)
|
|
21
|
+
_SOURCE_TIMEOUT = 5
|
|
22
|
+
_ANTIGRAVITY_RELEASES = "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases"
|
|
18
23
|
_PONYTAIL_URL = "https://github.com/DietrichGebert/ponytail.git"
|
|
19
24
|
|
|
20
25
|
|
|
@@ -41,45 +46,88 @@ def _run(command, env, runner=None, timeout=15):
|
|
|
41
46
|
return {"returncode": result.returncode, "stdout": result.stdout or "", "stderr": result.stderr or ""}
|
|
42
47
|
|
|
43
48
|
|
|
49
|
+
def _parse_version(value):
|
|
50
|
+
if isinstance(value, str) and re.fullmatch(r"v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", value):
|
|
51
|
+
return value.lstrip("v")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
44
55
|
def _version(command, env, runner):
|
|
45
|
-
result = _run([command, "--version"], env, runner)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
result = _run([command, "--version"], env, runner, timeout=_SOURCE_TIMEOUT)
|
|
57
|
+
if result["returncode"]:
|
|
58
|
+
return None
|
|
59
|
+
text = "\n".join((result["stdout"] or "", result["stderr"] or "")).strip()
|
|
60
|
+
# Ollama can print the server version before the CLI version.
|
|
61
|
+
client = re.search(r"client version is (\S+)", text)
|
|
62
|
+
match = client or re.search(r"\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b", text)
|
|
63
|
+
return _parse_version(match.group(1)) if match else None
|
|
49
64
|
|
|
50
65
|
|
|
51
|
-
def
|
|
52
|
-
|
|
66
|
+
def _fetch_json(url):
|
|
67
|
+
request = Request(url, headers={"Accept": "application/json", "User-Agent": "cdx-manager-update-check"})
|
|
68
|
+
with urlopen(request, timeout=_SOURCE_TIMEOUT) as response:
|
|
69
|
+
return json.loads(response.read(1024 * 1024))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _latest_version(name):
|
|
73
|
+
try:
|
|
74
|
+
if name in (PROVIDER_CODEX, PROVIDER_CLAUDE):
|
|
75
|
+
package = "@openai/codex" if name == PROVIDER_CODEX else "@anthropic-ai/claude-code"
|
|
76
|
+
return _parse_version(_fetch_json(f"https://registry.npmjs.org/{package}/latest")["version"])
|
|
77
|
+
if name == "rtk":
|
|
78
|
+
release = _fetch_json("https://api.github.com/repos/rtk-ai/rtk/releases/latest")
|
|
79
|
+
if release.get("draft") or release.get("prerelease"):
|
|
80
|
+
return None
|
|
81
|
+
return _parse_version(release["tag_name"])
|
|
82
|
+
if name == PROVIDER_ANTIGRAVITY:
|
|
83
|
+
# agy belongs to the original editor, whose feed differs from Hub and IDE 2.
|
|
84
|
+
return _parse_version(_fetch_json(_ANTIGRAVITY_RELEASES)[0]["version"])
|
|
85
|
+
if name == PROVIDER_OLLAMA:
|
|
86
|
+
arch = {"x86_64": "amd64", "aarch64": "arm64"}.get(platform.machine().lower(), platform.machine().lower())
|
|
87
|
+
query = urlencode({"os": platform.system().lower(), "arch": arch, "version": "0.0.0"})
|
|
88
|
+
try:
|
|
89
|
+
release = _fetch_json(f"https://ollama.com/api/update?{query}")
|
|
90
|
+
# The vendor updater extracts the release tag from the artifact URL.
|
|
91
|
+
version = _parse_version(os.path.basename(os.path.dirname(urlparse(release["url"]).path)))
|
|
92
|
+
if version:
|
|
93
|
+
return version
|
|
94
|
+
except Exception:
|
|
95
|
+
# Linux can return HTTP 204 with no JSON body.
|
|
96
|
+
pass
|
|
97
|
+
release = _fetch_json("https://api.github.com/repos/ollama/ollama/releases/latest")
|
|
98
|
+
if release.get("draft") or release.get("prerelease"):
|
|
99
|
+
return None
|
|
100
|
+
return _parse_version(release["tag_name"])
|
|
101
|
+
except Exception:
|
|
102
|
+
# A failed or changed source is uncertainty, never evidence of currency.
|
|
53
103
|
return None
|
|
54
|
-
|
|
55
|
-
value = (result["stdout"] or "").strip()
|
|
56
|
-
return value if re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", value) else None
|
|
104
|
+
return None
|
|
57
105
|
|
|
58
106
|
|
|
59
107
|
def _newer(latest, current):
|
|
60
108
|
def key(value):
|
|
61
|
-
return tuple(int(part) for part in
|
|
109
|
+
return tuple(int(part) for part in value.split("-", 1)[0].split("+", 1)[0].split("."))
|
|
62
110
|
return bool(latest and current and key(latest) > key(current))
|
|
63
111
|
|
|
64
112
|
|
|
65
|
-
def
|
|
66
|
-
if
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return
|
|
113
|
+
def _native_update_command(name, command, path, env, runner):
|
|
114
|
+
if name == PROVIDER_OLLAMA and platform.system().lower() == "linux":
|
|
115
|
+
return None
|
|
116
|
+
real_path = os.path.realpath(path)
|
|
117
|
+
cask = re.search(r"/Caskroom/([^/]+)/", real_path)
|
|
118
|
+
formula = re.search(r"/Cellar/([^/]+)/", real_path)
|
|
119
|
+
if (cask or formula) and _which("brew", env):
|
|
120
|
+
package = (cask or formula).group(1)
|
|
121
|
+
return ["brew", "upgrade", *(["--cask"] if cask else []), package]
|
|
122
|
+
if name == PROVIDER_CODEX:
|
|
123
|
+
return [command, "update"]
|
|
124
|
+
help_result = _run([command, "--help"], env, runner, timeout=_SOURCE_TIMEOUT)
|
|
125
|
+
if help_result["returncode"] == 0:
|
|
126
|
+
help_text = "\n".join((help_result["stdout"] or "", help_result["stderr"] or ""))
|
|
127
|
+
for subcommand in ("self-update", "update", "--update"):
|
|
128
|
+
if re.search(r"(?m)^\s*" + re.escape(subcommand) + r"(?:\s|$)", help_text):
|
|
129
|
+
return [command, subcommand]
|
|
130
|
+
return None
|
|
83
131
|
|
|
84
132
|
|
|
85
133
|
def _ponytail_revision(auth_home, env=None, runner=None):
|
|
@@ -99,26 +147,30 @@ def _ponytail_revision(auth_home, env=None, runner=None):
|
|
|
99
147
|
|
|
100
148
|
def collect_update_all_plan(service, env=None, runner=None):
|
|
101
149
|
env = dict(env or os.environ)
|
|
102
|
-
outdated = _brew_outdated(env, runner)
|
|
103
150
|
items, steps = [], []
|
|
104
|
-
for name, command
|
|
151
|
+
for name, command in _TOOLS:
|
|
105
152
|
path = _which(command, env)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
current = _version(command, env, runner) if path else None
|
|
154
|
+
item = {"name": name, "command": command, "installed": bool(path), "path": path,
|
|
155
|
+
"version": current, "latest_version": None, "update_command": None, "status": "not_installed"}
|
|
156
|
+
if path:
|
|
157
|
+
latest = _latest_version(name)
|
|
158
|
+
item["latest_version"] = latest
|
|
159
|
+
if latest and current and latest == current:
|
|
160
|
+
item["status"] = "up_to_date"
|
|
161
|
+
elif _newer(latest, current):
|
|
162
|
+
item["status"] = "update_available"
|
|
163
|
+
item["update_command"] = _native_update_command(name, command, path, env, runner)
|
|
164
|
+
if item["update_command"]:
|
|
165
|
+
steps.append({"name": name, "command": item["update_command"], "env": {}})
|
|
166
|
+
else:
|
|
167
|
+
item["reason"] = "No supported CLI updater or verified Homebrew installation; use the vendor's updater manually."
|
|
168
|
+
if name == PROVIDER_OLLAMA and platform.system().lower() == "linux":
|
|
169
|
+
item["reason"] = ("No supported safe automatic Linux update: Ollama's installer writes system paths and may restart a service. "
|
|
170
|
+
"Even a user-local binary may serve running models; arrange a manual maintenance window.")
|
|
171
|
+
else:
|
|
172
|
+
item["status"] = "unknown"
|
|
173
|
+
item["reason"] = "Latest version unavailable from the official source." if not latest else "Installed version unavailable or does not match the published release."
|
|
122
174
|
items.append(item)
|
|
123
175
|
|
|
124
176
|
rtk_available = any(item["name"] == "rtk" and item["installed"] for item in items)
|
|
@@ -131,13 +183,14 @@ def collect_update_all_plan(service, env=None, runner=None):
|
|
|
131
183
|
ponytail_head = None
|
|
132
184
|
if any(_ponytail_revision(session.get("authHome") or "", env, runner) for session in codex_sessions):
|
|
133
185
|
remote = _run(["git", "ls-remote", _PONYTAIL_URL, "HEAD"], env, runner)
|
|
134
|
-
|
|
186
|
+
candidate = (remote["stdout"] or "").split("\t", 1)[0].strip()
|
|
187
|
+
ponytail_head = candidate if remote["returncode"] == 0 and re.fullmatch(r"[0-9a-f]{40}", candidate) else None
|
|
135
188
|
for session in codex_sessions:
|
|
136
189
|
revision = _ponytail_revision(session.get("authHome") or "", env, runner)
|
|
137
190
|
row = {"session": session["name"], "installed": bool(revision), "revision": revision}
|
|
138
191
|
if revision:
|
|
139
192
|
row["latest_revision"] = ponytail_head
|
|
140
|
-
row["status"] = "update_available" if ponytail_head and not ponytail_head.startswith(revision) else "up_to_date"
|
|
193
|
+
row["status"] = "update_available" if ponytail_head and not ponytail_head.startswith(revision) else "up_to_date" if ponytail_head else "unknown"
|
|
141
194
|
if row["status"] == "update_available":
|
|
142
195
|
steps.append({"name": f"update Ponytail ({session['name']})", "command": ["codex", "plugin", "marketplace", "upgrade", "ponytail", "--json"], "env": {"CODEX_HOME": session["authHome"]}})
|
|
143
196
|
else:
|
|
@@ -176,6 +229,15 @@ def apply_update_all_plan(plan, service, env=None, runner=None, progress=None):
|
|
|
176
229
|
continue
|
|
177
230
|
result = _run(step["command"], {**env, **step.get("env", {})}, runner, timeout=120)
|
|
178
231
|
result = {"name": step["name"], "command": step["command"], **result}
|
|
232
|
+
item = next((item for item in plan.get("items", []) if item["name"] == step["name"]), None)
|
|
233
|
+
if result["returncode"] == 0 and item:
|
|
234
|
+
after = _version(item["command"], env, runner)
|
|
235
|
+
result.update(version_before=item["version"], version_after=after, latest_version=item["latest_version"])
|
|
236
|
+
if not (_newer(after, item["version"]) and
|
|
237
|
+
(after == item["latest_version"] or _newer(after, item["latest_version"]))):
|
|
238
|
+
result["returncode"] = 1
|
|
239
|
+
result["reason"] = "Update verification failed: installed version did not advance to the published release."
|
|
240
|
+
result["stderr"] = (result["stderr"] + "\n" + result["reason"]).strip()
|
|
179
241
|
results.append(result)
|
|
180
242
|
if result["returncode"] == 0 and step.get("id"):
|
|
181
243
|
completed.add(step["id"])
|