cdx-manager 0.18.5 → 0.18.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 +8 -7
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_18_6.md +8 -0
- package/checksums/release-archives.json +11 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +4 -2
- package/src/cli_args.py +2 -1
- package/src/commands/launch.py +56 -5
- package/src/commands/maintenance.py +2 -0
- package/src/commands/tray.py +7 -0
- package/src/session_service.py +2 -0
- package/src/session_status.py +4 -1
- package/src/tray_logics.py +41 -19
- package/src/tray_plugins.py +4 -4
- package/src/update_manager.py +1 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
|
|
10
10
|
|
|
@@ -377,12 +377,13 @@ It is off by default because turning it on writes into the provider's own
|
|
|
377
377
|
configuration and, on Codex, puts an approval prompt in front of you. That is
|
|
378
378
|
not something to do to sessions you already own without being asked.
|
|
379
379
|
|
|
380
|
-
|
|
381
|
-
notification includes at most 180 characters
|
|
382
|
-
a permission request may quote the
|
|
383
|
-
it wants to do. That text can be
|
|
384
|
-
|
|
385
|
-
|
|
380
|
+
New sessions inherit response previews when agent alerts were accepted during
|
|
381
|
+
tray installation. A completion notification includes at most 180 characters
|
|
382
|
+
of the agent's final response, and a permission request may quote the
|
|
383
|
+
provider's own one-line description of what it wants to do. That text can be
|
|
384
|
+
visible on a lock screen; disable it per session with
|
|
385
|
+
`cdx set <name> --notify-preview off`. Previews never read transcripts, and the
|
|
386
|
+
command a permission request is about is never included.
|
|
386
387
|
|
|
387
388
|
Both providers are subscribed to the same two hooks: `Stop` when a turn ends and
|
|
388
389
|
`PermissionRequest` when one blocks on your approval. Claude Code is also
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.18.
|
|
1
|
+
0.18.6
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# CDX Manager 0.18.6
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
- Launches can use an explicit `--dir`; each run records its directory for status and tray surfaces.
|
|
6
|
+
- The Logics tray card derives repositories from active runs and shows basename-only rows.
|
|
7
|
+
- `cdx update` names its install prefix, while `cdx tray doctor` reports the companion CDX command and hook-store alignment.
|
|
8
|
+
- Windows Terminal, GNOME Terminal, and Konsole open new tabs where their documented interfaces support it; other terminals retain the working window fallback.
|
|
@@ -246,6 +246,17 @@
|
|
|
246
246
|
"x86_64-pc-windows-msvc": "cc33c0f5a53472c6c1c72f458f33052abc09b6fe4ff5dfb5edae497c234bb2d0",
|
|
247
247
|
"x86_64-unknown-linux-musl": "d8fb9958112329487bd91be7f7505f18f886676f4309e30aa3877340e42c6c15"
|
|
248
248
|
}
|
|
249
|
+
},
|
|
250
|
+
"v0.18.5": {
|
|
251
|
+
"github_tarball_sha256": "d4251f1bdeb5aba31308cf749bf51733435bf2559f50a923ce090f0940c59876",
|
|
252
|
+
"github_zip_sha256": "36bc9d4e0a3cad306ccd405d7b4e4091418e21fe897b5ebcad4e275133615837",
|
|
253
|
+
"tray_assets": {
|
|
254
|
+
"aarch64-apple-darwin": "b0eb6653c3bbe1aba93d8b1966377e379eecea063ad01a10a11cdfd52d48e5ac",
|
|
255
|
+
"aarch64-unknown-linux-musl": "0cbca29099f1d320e917cb60e463fe0eb8fef9b78396db32520b8574ae4bb4a3",
|
|
256
|
+
"x86_64-apple-darwin": "a3e3d485552191f2456573dfbfee4259e430e7432b6d307eea00d5c527c86011",
|
|
257
|
+
"x86_64-pc-windows-msvc": "31a1743b0b6ec420a85c1679e93fcbd32c982a36e080a59d563276204682f152",
|
|
258
|
+
"x86_64-unknown-linux-musl": "49fba26d3deeab1e46af542528a5c71262e6c4d82898fcf989d7222a09a11d59"
|
|
259
|
+
}
|
|
249
260
|
}
|
|
250
261
|
}
|
|
251
262
|
}
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -619,8 +619,10 @@ def main(argv, options=None):
|
|
|
619
619
|
if handler:
|
|
620
620
|
return handler(rest, ctx)
|
|
621
621
|
|
|
622
|
-
|
|
623
|
-
|
|
622
|
+
from .commands.launch import _parse_launch_args
|
|
623
|
+
directory, launch_args = _parse_launch_args(rest)
|
|
624
|
+
if all(arg in ("--json", "-r", "--resume") for arg in launch_args):
|
|
625
|
+
return handle_launch(command, ctx, resume=("-r" in launch_args or "--resume" in launch_args), directory=directory)
|
|
624
626
|
|
|
625
627
|
raise CdxError(f"Unknown command: {command}. Use cdx --help.")
|
|
626
628
|
|
package/src/cli_args.py
CHANGED
|
@@ -43,7 +43,7 @@ RESUME_USAGE = "Usage: cdx resume <name> [--json]"
|
|
|
43
43
|
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
|
|
44
44
|
SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
|
|
45
45
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
46
|
-
RUN_USAGE = "Usage: cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json"
|
|
46
|
+
RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json"
|
|
47
47
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
48
48
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
49
49
|
RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
|
|
@@ -585,6 +585,7 @@ def _parse_run_effort(reasoning_effort, power):
|
|
|
585
585
|
def _parse_run_args(args):
|
|
586
586
|
parsed = _parse_run_flag_args(args, {
|
|
587
587
|
"--cwd": {"key": "cwd", "type": "str", "default": None},
|
|
588
|
+
"--dir": {"key": "cwd", "type": "str", "default": None},
|
|
588
589
|
"--prompt-file": {"key": "prompt_file", "type": "str", "default": None},
|
|
589
590
|
"--prompt": {"key": "prompt", "type": "str", "default": None},
|
|
590
591
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": _parse_run_provider},
|
package/src/commands/launch.py
CHANGED
|
@@ -28,6 +28,54 @@ from ..errors import CdxError
|
|
|
28
28
|
from ..provider_runtime import _ensure_session_authentication, _get_auth_home, _run_interactive_provider_command
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _parse_launch_args(args):
|
|
32
|
+
"""Return the per-launch directory and ordinary launch flags."""
|
|
33
|
+
directory = None
|
|
34
|
+
cleaned = []
|
|
35
|
+
index = 0
|
|
36
|
+
while index < len(args):
|
|
37
|
+
arg = args[index]
|
|
38
|
+
if arg == "--dir":
|
|
39
|
+
if directory is not None or index + 1 >= len(args):
|
|
40
|
+
raise CdxError("Usage: cdx <name> [--dir PATH] [--resume] [--json]")
|
|
41
|
+
directory = args[index + 1]
|
|
42
|
+
index += 2
|
|
43
|
+
continue
|
|
44
|
+
if arg.startswith("--dir="):
|
|
45
|
+
if directory is not None or not arg.split("=", 1)[1]:
|
|
46
|
+
raise CdxError("Usage: cdx <name> [--dir PATH] [--resume] [--json]")
|
|
47
|
+
directory = arg.split("=", 1)[1]
|
|
48
|
+
index += 1
|
|
49
|
+
continue
|
|
50
|
+
cleaned.append(arg)
|
|
51
|
+
index += 1
|
|
52
|
+
return directory, cleaned
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _launch_directory(session, ctx, explicit, json_flag):
|
|
56
|
+
cwd = os.path.realpath(explicit) if explicit else (ctx.get("cwd") or os.getcwd())
|
|
57
|
+
if explicit and not os.path.isdir(cwd):
|
|
58
|
+
raise CdxError(f"Invalid directory: {explicit or cwd}")
|
|
59
|
+
if explicit or json_flag or not ctx["stdin_is_tty"] or cwd not in {os.path.realpath(os.path.expanduser("~")), os.path.realpath(os.path.sep)}:
|
|
60
|
+
return cwd
|
|
61
|
+
recent = []
|
|
62
|
+
for entry in ctx["service"]["get_launch_history"](session["name"], limit=20):
|
|
63
|
+
path = entry.get("cwd")
|
|
64
|
+
if path and os.path.isdir(path) and path not in recent:
|
|
65
|
+
recent.append(path)
|
|
66
|
+
choices = recent + [cwd]
|
|
67
|
+
if len(choices) == 1:
|
|
68
|
+
return cwd
|
|
69
|
+
ctx["out"]("Choose launch directory:\n" + "".join(f" {index + 1}. {path}\n" for index, path in enumerate(choices)))
|
|
70
|
+
answer = (ctx["options"].get("input") or input)(f"Directory [1-{len(choices)}] (default {len(choices)}): ").strip()
|
|
71
|
+
if not answer:
|
|
72
|
+
return cwd
|
|
73
|
+
try:
|
|
74
|
+
return choices[int(answer) - 1]
|
|
75
|
+
except (ValueError, IndexError):
|
|
76
|
+
raise CdxError("Launch cancelled: choose a listed directory.") from None
|
|
77
|
+
|
|
78
|
+
|
|
31
79
|
def _format_resume_capability(capability, use_color=False):
|
|
32
80
|
name = capability["session"]
|
|
33
81
|
provider = capability["provider"]
|
|
@@ -74,12 +122,13 @@ def handle_can_resume(rest, ctx):
|
|
|
74
122
|
return 0
|
|
75
123
|
|
|
76
124
|
def handle_resume(rest, ctx):
|
|
125
|
+
directory, rest = _parse_launch_args(rest)
|
|
77
126
|
json_flag, args = _parse_json_flag(rest)
|
|
78
127
|
if len(args) != 1:
|
|
79
128
|
raise CdxError(RESUME_USAGE)
|
|
80
|
-
return handle_launch(args[0], ctx, resume=True, force_json=json_flag)
|
|
129
|
+
return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory)
|
|
81
130
|
|
|
82
|
-
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None):
|
|
131
|
+
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None):
|
|
83
132
|
json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
|
|
84
133
|
if force_json is not None:
|
|
85
134
|
json_flag = force_json
|
|
@@ -91,6 +140,9 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
91
140
|
raise CdxError(
|
|
92
141
|
f"Provider {session['provider']} does not support native resume through cdx."
|
|
93
142
|
)
|
|
143
|
+
cwd = _launch_directory(session, ctx, directory, json_flag)
|
|
144
|
+
if not json_flag and not ctx["stdin_is_tty"]:
|
|
145
|
+
ctx["out"](f"Using directory: {cwd}\n")
|
|
94
146
|
_ensure_session_authentication(
|
|
95
147
|
session,
|
|
96
148
|
ctx["service"],
|
|
@@ -127,13 +179,12 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
127
179
|
# a real turn — so it gets no instruction it does not need.
|
|
128
180
|
notice += " — approve them once in Codex to enable"
|
|
129
181
|
ctx["out"](f"{_info(notice, ctx['use_color'])}\n")
|
|
130
|
-
cwd = ctx.get("cwd") or os.getcwd()
|
|
131
182
|
runtime_run_id = None
|
|
132
183
|
|
|
133
184
|
def runtime_lifecycle(event, info):
|
|
134
185
|
nonlocal runtime_run_id
|
|
135
186
|
if event == "started":
|
|
136
|
-
runtime = ctx["service"]["start_session_runtime"](session["name"], info)
|
|
187
|
+
runtime = ctx["service"]["start_session_runtime"](session["name"], {**info, "cwd": cwd})
|
|
137
188
|
runtime_run_id = runtime.get("runId")
|
|
138
189
|
elif event == "finished" and runtime_run_id:
|
|
139
190
|
ctx["service"]["finish_session_runtime"](
|
|
@@ -176,7 +227,7 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
176
227
|
**run_info,
|
|
177
228
|
})
|
|
178
229
|
if json_flag:
|
|
179
|
-
extra = {"session": ctx["service"]["get_session"](session["name"])}
|
|
230
|
+
extra = {"session": ctx["service"]["get_session"](session["name"]), "cwd": cwd}
|
|
180
231
|
if capability:
|
|
181
232
|
extra["resume"] = capability
|
|
182
233
|
_write_json(ctx, _json_success("resume" if resume else "launch", message, warnings=warnings, **extra))
|
|
@@ -774,10 +774,12 @@ def handle_update(rest, ctx):
|
|
|
774
774
|
current_version=current_version,
|
|
775
775
|
target_version=target_version,
|
|
776
776
|
mode=plan["mode"],
|
|
777
|
+
prefix=plan["prefix"],
|
|
777
778
|
steps=results,
|
|
778
779
|
))
|
|
779
780
|
return 0
|
|
780
781
|
ctx["out"](f"{_success(message, ctx['use_color'])}\n")
|
|
782
|
+
ctx["out"](f"Install prefix: {plan['prefix']}\n")
|
|
781
783
|
for warning in warnings:
|
|
782
784
|
ctx["out"](f"{_warn(warning['message'], ctx['use_color'])}\n")
|
|
783
785
|
return 0
|
package/src/commands/tray.py
CHANGED
|
@@ -5,12 +5,14 @@ and `autostart` act on recorded state and say plainly when there is none, and
|
|
|
5
5
|
`doctor` reads all of it without changing any of it.
|
|
6
6
|
"""
|
|
7
7
|
import os
|
|
8
|
+
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
|
|
11
12
|
from ..cli_args import TRAY_USAGE, _parse_flag_args
|
|
12
13
|
from ..cli_helpers import _json_success, _write_json
|
|
13
14
|
from ..cli_render import _dim, _pad_table, _style, _warn
|
|
15
|
+
from ..config import get_cdx_home
|
|
14
16
|
from ..errors import CdxError
|
|
15
17
|
from ..tray_alerts import alerts_enabled, set_alerts
|
|
16
18
|
from ..tray_autostart import disable as autostart_disable
|
|
@@ -127,6 +129,7 @@ def _tray_status(args, ctx):
|
|
|
127
129
|
ctx["service"]["base_dir"],
|
|
128
130
|
env=ctx.get("env"),
|
|
129
131
|
cache=None if parsed["refresh"] else _plugin_cache(ctx),
|
|
132
|
+
directories=[row.get("cwd") for row in rows if row.get("active") and row.get("cwd")],
|
|
130
133
|
)
|
|
131
134
|
snapshot = build_snapshot(
|
|
132
135
|
rows,
|
|
@@ -591,6 +594,8 @@ def _tray_doctor(args, ctx):
|
|
|
591
594
|
instance = companion_instance(env=env)
|
|
592
595
|
desktop = desktop_capability(env=env)
|
|
593
596
|
toasts = toast_capability(env=env)
|
|
597
|
+
companion_cdx = (env or {}).get("CDX_TRAY_CDX") or shutil.which("cdx", path=(env or {}).get("PATH")) or "cdx"
|
|
598
|
+
hook_store = get_cdx_home(env)
|
|
594
599
|
|
|
595
600
|
checks = [
|
|
596
601
|
("companion", "installed" if state else ("override" if executable else "absent"),
|
|
@@ -608,6 +613,8 @@ def _tray_doctor(args, ctx):
|
|
|
608
613
|
("autostart", "on" if autostart["enabled"] else "off", autostart["artifact"] or "unsupported"),
|
|
609
614
|
("alerts", "on" if alerts_enabled(base_dir) else "muted",
|
|
610
615
|
"cdx tray alerts on|off"),
|
|
616
|
+
("companion_cdx", "override" if (env or {}).get("CDX_TRAY_CDX") else "path", companion_cdx),
|
|
617
|
+
("hook_store", "aligned" if os.path.realpath(hook_store) == os.path.realpath(base_dir) else "mismatched", hook_store),
|
|
611
618
|
("update", "interrupted" if interrupted_update(base_dir) else "clean",
|
|
612
619
|
"a staged companion was never promoted; run: cdx tray install"
|
|
613
620
|
if interrupted_update(base_dir) else "-"),
|
package/src/session_service.py
CHANGED
|
@@ -629,6 +629,7 @@ def _launch_defaults_for(base_dir, provider):
|
|
|
629
629
|
try:
|
|
630
630
|
if supports_agent_alerts(provider) and alerts_default(base_dir):
|
|
631
631
|
launch["notify"] = True
|
|
632
|
+
launch["notify_preview"] = True
|
|
632
633
|
except OSError:
|
|
633
634
|
# An unreadable default is no default. Creating the session matters
|
|
634
635
|
# more than inheriting a preference.
|
|
@@ -739,6 +740,7 @@ def start_session_runtime(store, name, payload=None):
|
|
|
739
740
|
"pid": payload.get("pid") or os.getpid(),
|
|
740
741
|
"command": payload.get("command"),
|
|
741
742
|
"label": payload.get("label"),
|
|
743
|
+
"cwd": payload.get("cwd"),
|
|
742
744
|
"transcriptPath": payload.get("transcript_path") or payload.get("transcriptPath"),
|
|
743
745
|
}
|
|
744
746
|
|
package/src/session_status.py
CHANGED
|
@@ -268,6 +268,7 @@ def _status_row_from_session(store, s):
|
|
|
268
268
|
status = s.get("lastStatus")
|
|
269
269
|
enabled = s.get("enabled", True) is not False
|
|
270
270
|
row_status = status if enabled else None
|
|
271
|
+
runtime = _session_runtime(store, s["name"])
|
|
271
272
|
return {
|
|
272
273
|
"session_name": s["name"],
|
|
273
274
|
"label": s.get("label"),
|
|
@@ -279,7 +280,8 @@ def _status_row_from_session(store, s):
|
|
|
279
280
|
# ignored `--priority` entirely.
|
|
280
281
|
"priority": _row_priority(s),
|
|
281
282
|
"reasoning_effort": _row_reasoning_effort(s),
|
|
282
|
-
"active": bool(
|
|
283
|
+
"active": bool(runtime) if enabled else False,
|
|
284
|
+
"cwd": runtime.get("cwd") if runtime else None,
|
|
283
285
|
"status": "enabled" if enabled else "disabled",
|
|
284
286
|
"auth_status": (s.get("auth") or {}).get("status") or "unknown",
|
|
285
287
|
"auth_checked_at": _to_local_iso((s.get("auth") or {}).get("lastCheckedAt")),
|
|
@@ -313,6 +315,7 @@ def format_list_rows(store):
|
|
|
313
315
|
"provider": s["provider"] if has_multiple else None,
|
|
314
316
|
"enabled": s.get("enabled", True) is not False,
|
|
315
317
|
"active": bool(_session_runtime(store, s["name"])) if s.get("enabled", True) is not False else False,
|
|
318
|
+
"cwd": (_session_runtime(store, s["name"]) or {}).get("cwd"),
|
|
316
319
|
"enabled_status": "disabled" if s.get("enabled", True) is False else "enabled",
|
|
317
320
|
"status": s.get("lastStatus"),
|
|
318
321
|
"launch": s.get("launch") or {},
|
package/src/tray_logics.py
CHANGED
|
@@ -12,6 +12,7 @@ task, because it is what to do instead. A tray menu that listed everything would
|
|
|
12
12
|
be a worse `logics-manager status`, run more often.
|
|
13
13
|
"""
|
|
14
14
|
import json
|
|
15
|
+
import os
|
|
15
16
|
import subprocess
|
|
16
17
|
import time
|
|
17
18
|
|
|
@@ -21,7 +22,7 @@ from .tray_plugins import ADAPTER_TIMEOUT_SECONDS, CARD_TTL_SECONDS
|
|
|
21
22
|
_PRIORITY_ORDER = {"high": 0, "medium": 1, "low": 2}
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def logics_card(env=None, now=None, cache=None, runner=None, executable=None):
|
|
25
|
+
def logics_card(env=None, now=None, cache=None, runner=None, executable=None, directories=None):
|
|
25
26
|
"""One card, or None when there is nothing truthful to show.
|
|
26
27
|
|
|
27
28
|
None covers every unhappy case on purpose — not installed, not a Logics
|
|
@@ -33,7 +34,7 @@ def logics_card(env=None, now=None, cache=None, runner=None, executable=None):
|
|
|
33
34
|
cached = _from_cache(cache, now)
|
|
34
35
|
if cached is not None:
|
|
35
36
|
return cached
|
|
36
|
-
status = _status(env=env, runner=runner, executable=executable)
|
|
37
|
+
status = _status(env=env, runner=runner, executable=executable, directories=directories)
|
|
37
38
|
card = _card_from_status(status)
|
|
38
39
|
_to_cache(cache, now, card)
|
|
39
40
|
return card
|
|
@@ -58,7 +59,16 @@ def _to_cache(cache, now, card):
|
|
|
58
59
|
cache["logics"] = {"at": now, "card": card}
|
|
59
60
|
|
|
60
61
|
|
|
61
|
-
def
|
|
62
|
+
def _repository_root(directory):
|
|
63
|
+
directory = os.path.realpath(directory or "")
|
|
64
|
+
while directory and directory != os.path.dirname(directory):
|
|
65
|
+
if os.path.exists(os.path.join(directory, ".git")):
|
|
66
|
+
return directory
|
|
67
|
+
directory = os.path.dirname(directory)
|
|
68
|
+
return directory if os.path.exists(os.path.join(directory, ".git")) else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _status(env=None, runner=None, executable=None, directories=None):
|
|
62
72
|
"""`logics-manager status --format json`, or None however it failed.
|
|
63
73
|
|
|
64
74
|
Resolved through CDX's own lookup rather than a path the tray knows: on a
|
|
@@ -68,26 +78,38 @@ def _status(env=None, runner=None, executable=None):
|
|
|
68
78
|
executable = executable or resolve_logics_manager(env or {})
|
|
69
79
|
if not executable:
|
|
70
80
|
return None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
[executable, "status", "--format", "json"],
|
|
74
|
-
capture_output=True,
|
|
75
|
-
text=True,
|
|
76
|
-
timeout=ADAPTER_TIMEOUT_SECONDS,
|
|
77
|
-
check=False,
|
|
78
|
-
)
|
|
79
|
-
except (OSError, subprocess.SubprocessError):
|
|
80
|
-
return None
|
|
81
|
-
if getattr(completed, "returncode", 1) != 0:
|
|
82
|
-
return None
|
|
83
|
-
try:
|
|
84
|
-
payload = json.loads(completed.stdout)
|
|
85
|
-
except (TypeError, ValueError):
|
|
81
|
+
roots = sorted({root for root in (_repository_root(path) for path in (directories or [])) if root})
|
|
82
|
+
if directories is not None and not roots:
|
|
86
83
|
return None
|
|
87
|
-
|
|
84
|
+
payloads = []
|
|
85
|
+
for root in roots or [None]:
|
|
86
|
+
try:
|
|
87
|
+
completed = (runner or subprocess.run)(
|
|
88
|
+
[executable, "status", "--format", "json"], capture_output=True,
|
|
89
|
+
text=True, timeout=ADAPTER_TIMEOUT_SECONDS, check=False, **({"cwd": root} if root else {}),
|
|
90
|
+
)
|
|
91
|
+
payload = json.loads(completed.stdout) if getattr(completed, "returncode", 1) == 0 else None
|
|
92
|
+
except (OSError, subprocess.SubprocessError, TypeError, ValueError):
|
|
93
|
+
return None if not payloads else payloads
|
|
94
|
+
if isinstance(payload, dict):
|
|
95
|
+
payloads.append((root, payload))
|
|
96
|
+
return payloads if roots else (payloads[0][1] if payloads else None)
|
|
88
97
|
|
|
89
98
|
|
|
90
99
|
def _card_from_status(status):
|
|
100
|
+
if isinstance(status, list):
|
|
101
|
+
cards = [(root, _card_from_status(payload), len(payload.get("blocked_docs") or [])) for root, payload in status]
|
|
102
|
+
cards = [(root, card, blocked) for root, card, blocked in cards if card]
|
|
103
|
+
if not cards:
|
|
104
|
+
return None
|
|
105
|
+
root, card, _ = sorted(cards, key=lambda item: (-item[2], -len(item[1]["rows"]), item[0] or ""))[0]
|
|
106
|
+
repository_count = len(cards)
|
|
107
|
+
blocked_count = sum(item[2] for item in cards)
|
|
108
|
+
card["summary"] = f"{repository_count} repositories · {blocked_count} blocked"
|
|
109
|
+
prefix = os.path.basename(root or "")
|
|
110
|
+
for row in card["rows"]:
|
|
111
|
+
row["label"] = f"{prefix}: {row['label']}"
|
|
112
|
+
return card
|
|
91
113
|
if not isinstance(status, dict):
|
|
92
114
|
return None
|
|
93
115
|
blocked = [doc for doc in (status.get("blocked_docs") or []) if isinstance(doc, dict)]
|
package/src/tray_plugins.py
CHANGED
|
@@ -128,7 +128,7 @@ def _valid_action(action, plugin):
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None):
|
|
131
|
+
def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None, directories=None):
|
|
132
132
|
"""Every enabled plugin's card, bounded, with failures dropped.
|
|
133
133
|
|
|
134
134
|
Never raises. An adapter that throws, hangs past its timeout, returns
|
|
@@ -143,7 +143,7 @@ def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None):
|
|
|
143
143
|
if not adapter:
|
|
144
144
|
continue
|
|
145
145
|
try:
|
|
146
|
-
raw = adapter(env=env, now=now, cache=cache)
|
|
146
|
+
raw = adapter(env=env, now=now, cache=cache, directories=directories) if name == "logics" else adapter(env=env, now=now, cache=cache)
|
|
147
147
|
except Exception: # noqa: BLE001 - an integration cannot break the tray
|
|
148
148
|
continue
|
|
149
149
|
card = bounded_card(raw, name)
|
|
@@ -152,10 +152,10 @@ def collect_cards(base_dir, env=None, now=None, adapters=None, cache=None):
|
|
|
152
152
|
return cards
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
def _logics_adapter(env=None, now=None, cache=None):
|
|
155
|
+
def _logics_adapter(env=None, now=None, cache=None, directories=None):
|
|
156
156
|
from .tray_logics import logics_card
|
|
157
157
|
|
|
158
|
-
return logics_card(env=env, now=now, cache=cache)
|
|
158
|
+
return logics_card(env=env, now=now, cache=cache, directories=directories)
|
|
159
159
|
|
|
160
160
|
|
|
161
161
|
# The registry. A name here is the only way an adapter can run, and adding one
|
package/src/update_manager.py
CHANGED