cdx-manager 0.12.0 → 0.12.1
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 +2 -1
- package/changelogs/CHANGELOGS_0_12_1.md +25 -0
- package/checksums/release-archives.json +8 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +2 -2
- package/src/cli_args.py +8 -3
- package/src/cli_commands.py +68 -0
- package/src/cli_helpers.py +9 -4
- package/src/update_all.py +184 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
4
4
|
|
|
5
5
|
**Run multiple Codex, Claude, Antigravity, and Ollama sessions from one terminal. Switch between accounts instantly.**
|
|
6
6
|
|
|
@@ -376,6 +376,7 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
|
|
|
376
376
|
| `cdx repair [--dry-run] [--force] [--json]` | Plan or apply safe repairs for missing state files, quarantines, and orphan profiles |
|
|
377
377
|
| `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 |
|
|
378
378
|
| `cdx update [--check] [--yes] [--json] [--version TAG]` | Update cdx-manager using the installer that matches how it was installed |
|
|
379
|
+
| `cdx update all [--yes] [--json]` | Check installed providers, RTK, and Ponytail across Codex profiles; show the plan and ask once before applying safe native/Homebrew updates and missing RTK/Ponytail setup |
|
|
379
380
|
| `cdx ready [--refresh] [--json]` | Schedule an OS notification for the next cooling-down assistant that becomes ready, then return immediately |
|
|
380
381
|
| `cdx notify <name> --at-reset [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait for a session reset time or schedule an OS wake-up notification when due |
|
|
381
382
|
| `cdx notify --next-ready [--poll seconds] [--once] [--schedule] [--refresh] [--json]` | Wait until the recommended session is usable, or schedule the next known reset notification |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# CDX Manager 0.12.1
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
- Added `cdx update all` to inventory and safely update the installed provider CLIs, RTK, and Ponytail across Codex profiles.
|
|
6
|
+
- Made account handoffs use native provider transcripts before terminal capture, preserving actionable conversation instead of terminal noise.
|
|
7
|
+
|
|
8
|
+
## Changes
|
|
9
|
+
|
|
10
|
+
### Workstation updates
|
|
11
|
+
|
|
12
|
+
`cdx update all` now reports installed versions and session setup, asks once before applying its plan, and shows progress plus the command and error output when an action fails. It follows the actual installer provenance of each CLI, including the Claude Code cask rather than the separate Claude desktop app.
|
|
13
|
+
|
|
14
|
+
RTK is enabled for sessions that lack it when available. Ponytail marketplaces and plugins are checked per Codex profile; current Codex marketplace caches without a `last_revision` field are resolved from their Git checkout.
|
|
15
|
+
|
|
16
|
+
### Reliable handoffs
|
|
17
|
+
|
|
18
|
+
Cross-session handoffs now prefer native JSONL transcripts and retain only user and assistant messages. Script-captured terminal logs remain a fallback, but no longer displace structured provider conversation or tool-output noise into the shared context.
|
|
19
|
+
|
|
20
|
+
## Validation
|
|
21
|
+
|
|
22
|
+
- `npm run lint`
|
|
23
|
+
- `npm test`
|
|
24
|
+
- `logics-manager lint --require-status`
|
|
25
|
+
- `logics-manager audit --group-by-doc`
|
|
@@ -148,6 +148,14 @@
|
|
|
148
148
|
"v0.11.2": {
|
|
149
149
|
"github_tarball_sha256": "513d7c586a662da5ac5daf792fc76f746e924bbd95157a9d0cbbeba34f2991e0",
|
|
150
150
|
"github_zip_sha256": "d6d633efcbdc28818f68bd87efe3f246fe2983a9f4aa24d03868c978645c593e"
|
|
151
|
+
},
|
|
152
|
+
"v0.12.0": {
|
|
153
|
+
"github_tarball_sha256": "de1bac90e790b5d557f90fd22f7dd317c45646d0e07320c05c8b2336ed9bbced",
|
|
154
|
+
"github_zip_sha256": "213f6217dfd8c7aa979af691c2d63598b4589ca40f36e816f4ae9c9f2da27073"
|
|
155
|
+
},
|
|
156
|
+
"v0.12.1": {
|
|
157
|
+
"github_tarball_sha256": "87e3b11886a8e7fb10233d425f6a1986e62395c89049947678a50cfb20384278",
|
|
158
|
+
"github_zip_sha256": "249f75eb240de7cc421f17579e0040d35eb06e3213568673f743c92191dda0f0"
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
161
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -78,7 +78,7 @@ from .status_view import (
|
|
|
78
78
|
)
|
|
79
79
|
from .update_check import check_for_update, check_logics_manager_for_update
|
|
80
80
|
|
|
81
|
-
VERSION = "0.12.
|
|
81
|
+
VERSION = "0.12.1"
|
|
82
82
|
|
|
83
83
|
# Public surface: this module is a facade. Names below are imported above
|
|
84
84
|
# purely to be re-exported (consumed by tests and external callers); listing
|
|
@@ -219,7 +219,7 @@ def _print_help(use_color=False):
|
|
|
219
219
|
f" {_style('cdx doctor [--json]', '36', use_color)}",
|
|
220
220
|
f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
|
|
221
221
|
f" {_style('cdx view [--json]', '36', use_color)}",
|
|
222
|
-
f" {_style('cdx update [--check] [--yes] [--json] [--version TAG]', '36', use_color)}",
|
|
222
|
+
f" {_style('cdx update [all] [--check] [--yes] [--json] [--version TAG]', '36', use_color)}",
|
|
223
223
|
f" {_style('cdx ready [--refresh] [--json]', '36', use_color)}",
|
|
224
224
|
f" {_style('cdx notify <name> --at-reset [--schedule] [--refresh] [--json]', '36', use_color)}",
|
|
225
225
|
f" {_style('cdx notify --next-ready [--schedule] [--refresh] [--json]', '36', use_color)}",
|
package/src/cli_args.py
CHANGED
|
@@ -15,7 +15,7 @@ STATUS_USAGE = "Usage: cdx status [--json] [--refresh|--cached] [--timeout SECON
|
|
|
15
15
|
DOCTOR_USAGE = "Usage: cdx doctor [--json]"
|
|
16
16
|
DISK_USAGE = "Usage: cdx disk [profiles] [--candidates] [--json]"
|
|
17
17
|
REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
|
|
18
|
-
UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
|
|
18
|
+
UPDATE_USAGE = "Usage: cdx update [all] [--check] [--yes] [--json] [--version TAG]"
|
|
19
19
|
EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
|
|
20
20
|
IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--allow-authless-force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
|
|
21
21
|
CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text...] [--json]"
|
|
@@ -594,11 +594,16 @@ def _parse_update_args(args):
|
|
|
594
594
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
595
595
|
"--yes": {"key": "yes", "type": "bool", "default": False},
|
|
596
596
|
"--version": {"key": "version", "type": "str", "default": None},
|
|
597
|
-
}, UPDATE_USAGE)
|
|
597
|
+
}, UPDATE_USAGE, positionals_key="values", max_positionals=1)
|
|
598
|
+
if parsed["values"] not in ([], ["all"]):
|
|
599
|
+
raise CdxError(UPDATE_USAGE)
|
|
600
|
+
parsed["all"] = bool(parsed["values"])
|
|
601
|
+
if parsed["all"] and (parsed["check"] or parsed["version"]):
|
|
602
|
+
raise CdxError("Usage: cdx update all [--yes] [--json]")
|
|
598
603
|
if parsed["check"] and parsed["version"]:
|
|
599
604
|
raise CdxError("Usage: cdx update --check cannot be combined with --version.")
|
|
600
605
|
if parsed["version"] is not None and not parsed["version"].strip():
|
|
601
|
-
raise CdxError(
|
|
606
|
+
raise CdxError(UPDATE_USAGE)
|
|
602
607
|
return parsed
|
|
603
608
|
|
|
604
609
|
|
package/src/cli_commands.py
CHANGED
|
@@ -124,10 +124,46 @@ from .run_command import read_run_prompt, run_cdx_error_code, run_result_payload
|
|
|
124
124
|
from .run_registry import RunRegistry, build_code_review_report
|
|
125
125
|
from .run_usage import extract_run_usage
|
|
126
126
|
from .status_view import _format_status_detail, _format_status_rows, format_priority_instruction, recommend_priority_rows
|
|
127
|
+
from .update_all import apply_update_all_plan, collect_update_all_plan
|
|
127
128
|
from .update_check import LatestReleaseCheckError, fetch_latest_release, fetch_latest_release_or_raise, is_newer_version
|
|
128
129
|
from .update_manager import build_update_plan, format_update_failure, run_update_plan, verify_updated_command
|
|
129
130
|
|
|
130
131
|
|
|
132
|
+
def _format_update_all(plan, use_color=False):
|
|
133
|
+
labels = {
|
|
134
|
+
"up_to_date": _success("OK", use_color),
|
|
135
|
+
"update_available": _warn("UPDATE", use_color),
|
|
136
|
+
"not_installed": _warn("MISSING", use_color),
|
|
137
|
+
"check_with_native_updater": _info("CHECK", use_color),
|
|
138
|
+
}
|
|
139
|
+
rows = [[_style("TOOL", "1", use_color), _style("CURRENT", "1", use_color), _style("LATEST", "1", use_color), _style("STATUS", "1", use_color)]]
|
|
140
|
+
for item in plan["items"]:
|
|
141
|
+
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("_", " "))])
|
|
142
|
+
lines = [_style("Workstation update", "1", use_color), _dim("Inventory only — nothing has changed.", use_color), "", _pad_table(rows)]
|
|
143
|
+
missing_rtk = plan["setup"]["rtk_missing_sessions"]
|
|
144
|
+
setup = [f"RTK: enable for {', '.join(missing_rtk)}" if missing_rtk else "RTK: configured for every session"]
|
|
145
|
+
setup.extend(f"Ponytail {plugin['session']}: {labels.get(plugin['status'], plugin['status'].replace('_', ' '))}" for plugin in plan["setup"]["ponytail"])
|
|
146
|
+
lines.extend(["", _style("Session setup", "1", use_color), *setup])
|
|
147
|
+
if plan["steps"]:
|
|
148
|
+
lines.extend(["", _warn(f"{len(plan['steps'])} action(s) ready", use_color), _dim("Confirm to update only the tools listed above.", use_color)])
|
|
149
|
+
else:
|
|
150
|
+
lines.extend(["", _success("Everything managed by CDX is current and configured.", use_color)])
|
|
151
|
+
return "\n".join(lines)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _format_update_all_result(result, use_color=False):
|
|
155
|
+
if result.get("skipped"):
|
|
156
|
+
return _warn(f"skipped: {result['name']} (blocked by {result['blocked_by']})", use_color)
|
|
157
|
+
if result.get("returncode") == 0:
|
|
158
|
+
return _success(f"done: {result['name']}", use_color)
|
|
159
|
+
detail = str(result.get("stderr") or result.get("stdout") or "No error output.").strip().replace("\n", " ")
|
|
160
|
+
detail = detail[-500:]
|
|
161
|
+
command = shlex.join(result.get("command") or [])
|
|
162
|
+
suffix = f"\n {_dim(command, use_color)}" if command else ""
|
|
163
|
+
label = _warn(f"failed: {result['name']} (exit {result.get('returncode')})", use_color)
|
|
164
|
+
return f"{label}\n {detail}{suffix}"
|
|
165
|
+
|
|
166
|
+
|
|
131
167
|
def _resolve_bundle_passphrase(ctx, env_var, prompt, confirm=False, use_stdin=False):
|
|
132
168
|
env = ctx.get("env", {})
|
|
133
169
|
if env_var:
|
|
@@ -2674,6 +2710,38 @@ def handle_logout(rest, ctx):
|
|
|
2674
2710
|
|
|
2675
2711
|
def handle_update(rest, ctx):
|
|
2676
2712
|
parsed = _parse_update_args(rest)
|
|
2713
|
+
if parsed["all"]:
|
|
2714
|
+
plan = collect_update_all_plan(ctx["service"], env=ctx.get("env"), runner=ctx["options"].get("runUpdateAll"))
|
|
2715
|
+
if not parsed["yes"]:
|
|
2716
|
+
if parsed["json"]:
|
|
2717
|
+
_write_json(ctx, _json_success("update.all", "Collected workstation update plan", plan=plan, apply_required=bool(plan["steps"])))
|
|
2718
|
+
return 0
|
|
2719
|
+
ctx["out"](f"{_format_update_all(plan, ctx['use_color'])}\n")
|
|
2720
|
+
if not plan["steps"]:
|
|
2721
|
+
return 0
|
|
2722
|
+
if not ctx["stdin_is_tty"]:
|
|
2723
|
+
raise CdxError("cdx update all requires an interactive terminal or --yes.")
|
|
2724
|
+
if input("Apply this plan? [y/N] ").strip().lower() not in ("y", "yes"):
|
|
2725
|
+
ctx["out"](f"{_warn('Cancelled.', ctx['use_color'])}\n")
|
|
2726
|
+
return 0
|
|
2727
|
+
def progress(event):
|
|
2728
|
+
if parsed["json"]:
|
|
2729
|
+
return
|
|
2730
|
+
step_name = event["step"]["name"]
|
|
2731
|
+
if event["phase"] == "start":
|
|
2732
|
+
progress_label = _info(f"[{event['index']}/{event['total']}]", ctx["use_color"])
|
|
2733
|
+
ctx["out"](f"{progress_label} {step_name}...\n")
|
|
2734
|
+
else:
|
|
2735
|
+
ctx["out"](f" {_format_update_all_result(event['result'], ctx['use_color'])}\n")
|
|
2736
|
+
|
|
2737
|
+
results = apply_update_all_plan(plan, ctx["service"], env=ctx.get("env"), runner=ctx["options"].get("runUpdateAll"), progress=progress)
|
|
2738
|
+
failed = [row for row in results if row.get("returncode") != 0]
|
|
2739
|
+
message = "Applied workstation update plan" if not failed else "Workstation update plan completed with failures"
|
|
2740
|
+
if parsed["json"]:
|
|
2741
|
+
_write_json(ctx, _json_success("update.all", message, plan=plan, results=results, failed=len(failed)))
|
|
2742
|
+
else:
|
|
2743
|
+
ctx["out"](f"{_success(message, ctx['use_color']) if not failed else _warn(message, ctx['use_color'])}\n")
|
|
2744
|
+
return 1 if failed else 0
|
|
2677
2745
|
json_flag = parsed["json"]
|
|
2678
2746
|
current_version = str(ctx.get("version") or "").strip()
|
|
2679
2747
|
release_fetcher = ctx["options"].get("fetchLatestRelease") or fetch_latest_release
|
package/src/cli_helpers.py
CHANGED
|
@@ -250,11 +250,10 @@ def _collect_native_handoff_transcript_paths(session):
|
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
def _latest_handoff_transcript_path(session):
|
|
253
|
-
launch_path = _latest_launch_transcript_path(session)
|
|
254
|
-
if launch_path:
|
|
255
|
-
return launch_path
|
|
256
253
|
native_paths = _collect_native_handoff_transcript_paths(session)
|
|
257
|
-
|
|
254
|
+
if native_paths:
|
|
255
|
+
return native_paths[0]
|
|
256
|
+
return _latest_launch_transcript_path(session)
|
|
258
257
|
|
|
259
258
|
|
|
260
259
|
def _collect_handoff_text_fragments(value):
|
|
@@ -281,6 +280,10 @@ def _collect_handoff_text_fragments(value):
|
|
|
281
280
|
def _jsonl_record_to_handoff_text(record):
|
|
282
281
|
if not isinstance(record, dict):
|
|
283
282
|
return None
|
|
283
|
+
if record.get("type") == "response_item":
|
|
284
|
+
record = record.get("payload")
|
|
285
|
+
if not isinstance(record, dict):
|
|
286
|
+
return None
|
|
284
287
|
message = record.get("message") if isinstance(record.get("message"), dict) else {}
|
|
285
288
|
role = (
|
|
286
289
|
message.get("role")
|
|
@@ -289,6 +292,8 @@ def _jsonl_record_to_handoff_text(record):
|
|
|
289
292
|
or record.get("sender")
|
|
290
293
|
or "entry"
|
|
291
294
|
)
|
|
295
|
+
if role not in ("user", "assistant"):
|
|
296
|
+
return None
|
|
292
297
|
text_sources = []
|
|
293
298
|
for key in ("content", "text", "payload"):
|
|
294
299
|
if key in record:
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Inventory and apply the safe, known updates for a CDX workstation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from .config import PROVIDER_ANTIGRAVITY, PROVIDER_CLAUDE, PROVIDER_CODEX, PROVIDER_OLLAMA
|
|
10
|
+
|
|
11
|
+
_TOOLS = (
|
|
12
|
+
(PROVIDER_CODEX, "codex", None, ["codex", "update"]),
|
|
13
|
+
(PROVIDER_CLAUDE, "claude", "claude", ["brew", "upgrade", "--cask", "claude"]),
|
|
14
|
+
(PROVIDER_ANTIGRAVITY, "agy", "antigravity", ["brew", "upgrade", "--cask", "antigravity"]),
|
|
15
|
+
(PROVIDER_OLLAMA, "ollama", "ollama", ["brew", "upgrade", "ollama"]),
|
|
16
|
+
("rtk", "rtk", "rtk", ["brew", "upgrade", "rtk"]),
|
|
17
|
+
)
|
|
18
|
+
_PONYTAIL_URL = "https://github.com/DietrichGebert/ponytail.git"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _which(command, env):
|
|
22
|
+
path = env.get("PATH")
|
|
23
|
+
resolved = shutil.which(command, path=path)
|
|
24
|
+
if resolved or not path:
|
|
25
|
+
return resolved
|
|
26
|
+
for directory in path.split(os.pathsep):
|
|
27
|
+
candidate = os.path.join(directory, command)
|
|
28
|
+
if os.path.isfile(candidate):
|
|
29
|
+
return candidate
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run(command, env, runner=None, timeout=15):
|
|
34
|
+
runner = runner or subprocess.run
|
|
35
|
+
try:
|
|
36
|
+
result = runner(command, env=env, capture_output=True, text=True, check=False, timeout=timeout)
|
|
37
|
+
except (OSError, subprocess.TimeoutExpired) as error:
|
|
38
|
+
return {"returncode": 1, "stdout": "", "stderr": str(error)}
|
|
39
|
+
if isinstance(result, dict):
|
|
40
|
+
return result
|
|
41
|
+
return {"returncode": result.returncode, "stdout": result.stdout or "", "stderr": result.stderr or ""}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _version(command, env, runner):
|
|
45
|
+
result = _run([command, "--version"], env, runner)
|
|
46
|
+
text = (result["stdout"] or result["stderr"] or "").strip()
|
|
47
|
+
match = re.search(r"\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b", text)
|
|
48
|
+
return match.group(1) if match else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _npm_latest(package, env, runner):
|
|
52
|
+
if not _which("npm", env):
|
|
53
|
+
return None
|
|
54
|
+
result = _run(["npm", "view", package, "version"], env, runner)
|
|
55
|
+
value = (result["stdout"] or "").strip()
|
|
56
|
+
return value if re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", value) else None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _newer(latest, current):
|
|
60
|
+
def key(value):
|
|
61
|
+
return tuple(int(part) for part in str(value).split("-", 1)[0].split("+", 1)[0].split("."))
|
|
62
|
+
return bool(latest and current and key(latest) > key(current))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _brew_outdated(env, runner):
|
|
66
|
+
if not _which("brew", env):
|
|
67
|
+
return {}
|
|
68
|
+
result = _run(["brew", "outdated", "--json=v2"], env, runner, timeout=30)
|
|
69
|
+
try:
|
|
70
|
+
payload = json.loads(result["stdout"])
|
|
71
|
+
except (TypeError, ValueError):
|
|
72
|
+
return {}
|
|
73
|
+
rows = {}
|
|
74
|
+
for group in ("formulae", "casks"):
|
|
75
|
+
for item in payload.get(group, []):
|
|
76
|
+
rows[item.get("name")] = item
|
|
77
|
+
return rows
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _brew_cask_for_path(path):
|
|
81
|
+
match = re.search(r"/Caskroom/([^/]+)/", os.path.realpath(path or ""))
|
|
82
|
+
return match.group(1) if match else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ponytail_revision(auth_home, env=None, runner=None):
|
|
86
|
+
try:
|
|
87
|
+
with open(os.path.join(auth_home, "config.toml"), encoding="utf-8") as handle:
|
|
88
|
+
text = handle.read()
|
|
89
|
+
except OSError:
|
|
90
|
+
text = ""
|
|
91
|
+
match = re.search(r'\[marketplaces\.ponytail\].*?last_revision\s*=\s*"([0-9a-f]+)"', text, re.S)
|
|
92
|
+
if match:
|
|
93
|
+
return match.group(1)
|
|
94
|
+
cache = os.path.join(auth_home, ".tmp", "marketplaces", "ponytail")
|
|
95
|
+
result = _run(["git", "-C", cache, "rev-parse", "HEAD"], env or os.environ, runner)
|
|
96
|
+
revision = (result["stdout"] or "").strip()
|
|
97
|
+
return revision if re.fullmatch(r"[0-9a-f]{40}", revision) else None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def collect_update_all_plan(service, env=None, runner=None):
|
|
101
|
+
env = dict(env or os.environ)
|
|
102
|
+
outdated = _brew_outdated(env, runner)
|
|
103
|
+
items, steps = [], []
|
|
104
|
+
for name, command, brew_name, update_command in _TOOLS:
|
|
105
|
+
path = _which(command, env)
|
|
106
|
+
item = {"name": name, "command": command, "installed": bool(path), "path": path, "version": _version(command, env, runner) if path else None}
|
|
107
|
+
cask_name = _brew_cask_for_path(path)
|
|
108
|
+
brew_target = cask_name or brew_name
|
|
109
|
+
brew_row = outdated.get(brew_target) if brew_target else None
|
|
110
|
+
if brew_row:
|
|
111
|
+
command_to_run = ["brew", "upgrade", "--cask", brew_target] if cask_name else update_command
|
|
112
|
+
item.update({"status": "update_available", "latest_version": brew_row.get("current_version"), "update_command": command_to_run, "package": brew_target})
|
|
113
|
+
steps.append({"name": name, "command": command_to_run, "env": {}})
|
|
114
|
+
elif path and name == PROVIDER_CODEX:
|
|
115
|
+
# Codex owns its installer; it is the only reliable updater across npm, Homebrew and standalone installs.
|
|
116
|
+
latest = _npm_latest("@openai/codex", env, runner)
|
|
117
|
+
item.update({"latest_version": latest, "status": "update_available" if _newer(latest, item["version"]) else "up_to_date" if latest else "check_with_native_updater", "update_command": update_command})
|
|
118
|
+
if item["status"] != "up_to_date":
|
|
119
|
+
steps.append({"name": name, "command": update_command, "env": {}})
|
|
120
|
+
else:
|
|
121
|
+
item["status"] = "up_to_date" if path else "not_installed"
|
|
122
|
+
items.append(item)
|
|
123
|
+
|
|
124
|
+
rtk_available = any(item["name"] == "rtk" and item["installed"] for item in items)
|
|
125
|
+
missing_rtk = [session["name"] for session in service["list_sessions"]() if not (session.get("launch") or {}).get("rtk")]
|
|
126
|
+
setup = {"rtk_available": rtk_available, "rtk_missing_sessions": missing_rtk, "ponytail": []}
|
|
127
|
+
if rtk_available and missing_rtk:
|
|
128
|
+
steps.append({"name": "enable RTK", "command": None, "sessions": missing_rtk})
|
|
129
|
+
|
|
130
|
+
codex_sessions = [session for session in service["list_sessions"]() if session.get("provider") == PROVIDER_CODEX]
|
|
131
|
+
ponytail_head = None
|
|
132
|
+
if any(_ponytail_revision(session.get("authHome") or "", env, runner) for session in codex_sessions):
|
|
133
|
+
remote = _run(["git", "ls-remote", _PONYTAIL_URL, "HEAD"], env, runner)
|
|
134
|
+
ponytail_head = (remote["stdout"] or "").split("\t", 1)[0].strip() or None
|
|
135
|
+
for session in codex_sessions:
|
|
136
|
+
revision = _ponytail_revision(session.get("authHome") or "", env, runner)
|
|
137
|
+
row = {"session": session["name"], "installed": bool(revision), "revision": revision}
|
|
138
|
+
if revision:
|
|
139
|
+
row["latest_revision"] = ponytail_head
|
|
140
|
+
row["status"] = "update_available" if ponytail_head and not ponytail_head.startswith(revision) else "up_to_date"
|
|
141
|
+
if row["status"] == "update_available":
|
|
142
|
+
steps.append({"name": f"update Ponytail ({session['name']})", "command": ["codex", "plugin", "marketplace", "upgrade", "ponytail", "--json"], "env": {"CODEX_HOME": session["authHome"]}})
|
|
143
|
+
else:
|
|
144
|
+
row["status"] = "not_installed"
|
|
145
|
+
marketplace_id = f"ponytail-marketplace:{session['name']}"
|
|
146
|
+
steps.extend([
|
|
147
|
+
{"id": marketplace_id, "name": f"add Ponytail marketplace ({session['name']})", "command": ["codex", "plugin", "marketplace", "add", _PONYTAIL_URL], "env": {"CODEX_HOME": session["authHome"]}},
|
|
148
|
+
{"name": f"install Ponytail ({session['name']})", "command": ["codex", "plugin", "add", "ponytail@ponytail", "--json"], "env": {"CODEX_HOME": session["authHome"]}, "requires": marketplace_id},
|
|
149
|
+
])
|
|
150
|
+
setup["ponytail"].append(row)
|
|
151
|
+
return {"items": items, "setup": setup, "steps": steps}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def apply_update_all_plan(plan, service, env=None, runner=None, progress=None):
|
|
155
|
+
env = dict(env or os.environ)
|
|
156
|
+
results = []
|
|
157
|
+
completed = set()
|
|
158
|
+
for index, step in enumerate(plan["steps"], start=1):
|
|
159
|
+
if progress:
|
|
160
|
+
progress({"phase": "start", "index": index, "total": len(plan["steps"]), "step": step})
|
|
161
|
+
if step.get("requires") and step["requires"] not in completed:
|
|
162
|
+
result = {"name": step["name"], "returncode": None, "skipped": True, "blocked_by": step["requires"]}
|
|
163
|
+
results.append(result)
|
|
164
|
+
if progress:
|
|
165
|
+
progress({"phase": "done", "index": index, "total": len(plan["steps"]), "step": step, "result": result})
|
|
166
|
+
continue
|
|
167
|
+
if step["command"] is None:
|
|
168
|
+
for name in step["sessions"]:
|
|
169
|
+
service["set_launch_settings"](name, {"rtk": True})
|
|
170
|
+
result = {"name": step["name"], "returncode": 0, "sessions": step["sessions"]}
|
|
171
|
+
results.append(result)
|
|
172
|
+
if step.get("id"):
|
|
173
|
+
completed.add(step["id"])
|
|
174
|
+
if progress:
|
|
175
|
+
progress({"phase": "done", "index": index, "total": len(plan["steps"]), "step": step, "result": result})
|
|
176
|
+
continue
|
|
177
|
+
result = _run(step["command"], {**env, **step.get("env", {})}, runner, timeout=120)
|
|
178
|
+
result = {"name": step["name"], "command": step["command"], **result}
|
|
179
|
+
results.append(result)
|
|
180
|
+
if result["returncode"] == 0 and step.get("id"):
|
|
181
|
+
completed.add(step["id"])
|
|
182
|
+
if progress:
|
|
183
|
+
progress({"phase": "done", "index": index, "total": len(plan["steps"]), "step": step, "result": result})
|
|
184
|
+
return results
|