cdx-manager 0.20.8 → 0.20.10
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 +4 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_20_10.md +71 -0
- package/changelogs/CHANGELOGS_0_20_9.md +39 -0
- package/checksums/release-archives.json +22 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/backup_bundle.py +78 -22
- package/src/claude_credentials.py +148 -0
- package/src/claude_usage.py +78 -0
- package/src/commands/backup.py +5 -0
- package/src/commands/runs.py +22 -5
- package/src/context_store.py +22 -3
- package/src/provider_runtime.py +30 -3
- package/src/session_backup.py +117 -23
- package/src/session_service.py +76 -34
- package/src/tray_install.py +86 -14
package/src/tray_install.py
CHANGED
|
@@ -226,7 +226,15 @@ def install(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, targe
|
|
|
226
226
|
# Best-effort, and after the companion is known to be on disk: the shortcut
|
|
227
227
|
# points at it. A failure here does not fail the install — the tray runs
|
|
228
228
|
# either way, only its toasts would go nowhere, and doctor reports that.
|
|
229
|
-
|
|
229
|
+
#
|
|
230
|
+
# Only the install that is about to be live writes it. A staged executable
|
|
231
|
+
# is going to move, and a shortcut recorded against the staged path is a
|
|
232
|
+
# broken Start Menu entry the moment it does; the promotion writes it
|
|
233
|
+
# against the final path instead.
|
|
234
|
+
shortcut = (
|
|
235
|
+
create_shortcut(executable, env=env) if record
|
|
236
|
+
else {"created": False, "path": None, "reason": "staged install; the promotion writes the shortcut"}
|
|
237
|
+
)
|
|
230
238
|
|
|
231
239
|
state = {
|
|
232
240
|
"version": STATE_VERSION,
|
|
@@ -292,9 +300,9 @@ def align_companion(
|
|
|
292
300
|
try:
|
|
293
301
|
result = update(
|
|
294
302
|
base_dir, version,
|
|
295
|
-
download=download, ledger_path=ledger_path, target=target, probe=probe,
|
|
303
|
+
download=download, ledger_path=ledger_path, target=target, probe=probe, env=env,
|
|
296
304
|
)
|
|
297
|
-
except TrayInstallError as error:
|
|
305
|
+
except (TrayInstallError, OSError) as error:
|
|
298
306
|
outcome = {
|
|
299
307
|
"aligned": False,
|
|
300
308
|
"reason": str(error),
|
|
@@ -407,7 +415,8 @@ def staged_dir(base_dir):
|
|
|
407
415
|
return os.path.join(state_dir(base_dir), "companion.staged")
|
|
408
416
|
|
|
409
417
|
|
|
410
|
-
def update(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, target=None, probe=None
|
|
418
|
+
def update(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, target=None, probe=None,
|
|
419
|
+
env=None):
|
|
411
420
|
"""Replace an installed companion without ever being left with none.
|
|
412
421
|
|
|
413
422
|
The order is the whole point. The replacement is downloaded, verified and
|
|
@@ -444,18 +453,43 @@ def update(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, target
|
|
|
444
453
|
live = os.path.join(state_dir(base_dir), "companion")
|
|
445
454
|
retired = previous_dir(base_dir)
|
|
446
455
|
shutil.rmtree(retired, ignore_errors=True)
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
456
|
+
retired_live = False
|
|
457
|
+
try:
|
|
458
|
+
if os.path.isdir(live):
|
|
459
|
+
os.rename(live, retired)
|
|
460
|
+
retired_live = True
|
|
461
|
+
os.rename(staged, live)
|
|
462
|
+
except OSError as error:
|
|
463
|
+
# A failure between the two renames used to leave the recorded
|
|
464
|
+
# executable pointing at nothing: the install claimed a live companion
|
|
465
|
+
# and the path was empty. Put the proven one back, and report as a
|
|
466
|
+
# TrayInstallError so the caller's restart path runs.
|
|
467
|
+
restored = True
|
|
468
|
+
if retired_live and not os.path.isdir(live):
|
|
469
|
+
try:
|
|
470
|
+
os.rename(retired, live)
|
|
471
|
+
except OSError:
|
|
472
|
+
restored = False
|
|
473
|
+
shutil.rmtree(staged, ignore_errors=True)
|
|
474
|
+
raise TrayInstallError(
|
|
475
|
+
f"Could not promote the staged tray companion for CDX {version}: {error}. "
|
|
476
|
+
+ ("The installed one was restored." if restored
|
|
477
|
+
else f"The previous companion is retained at {retired}.")
|
|
478
|
+
) from error
|
|
450
479
|
# The retired copy stays. It used to be deleted here, which left nothing to
|
|
451
480
|
# go back to the moment the replacement turned out not to start — and that
|
|
452
481
|
# is precisely when it is needed. The next update clears it, so at most one
|
|
453
482
|
# extra copy is ever on disk.
|
|
454
483
|
|
|
484
|
+
live_executable = fresh["executable"].replace(staged, live, 1)
|
|
485
|
+
# Written now, against the path that will still exist afterwards, and
|
|
486
|
+
# recorded so uninstall keeps owning it.
|
|
487
|
+
shortcut = create_shortcut(live_executable, env=env)
|
|
455
488
|
state = {
|
|
456
489
|
**fresh,
|
|
457
|
-
"executable":
|
|
458
|
-
"paths": [live],
|
|
490
|
+
"executable": live_executable,
|
|
491
|
+
"paths": [live] + ([shortcut["path"]] if shortcut["created"] else []),
|
|
492
|
+
"shortcut": shortcut["path"] if shortcut["created"] else None,
|
|
459
493
|
}
|
|
460
494
|
_record_state(base_dir, state)
|
|
461
495
|
return {"state": state, "replaced": previous.get("cdx_version"), "previous_state": previous}
|
|
@@ -496,17 +530,55 @@ def restore_previous(base_dir, previous_state):
|
|
|
496
530
|
return False
|
|
497
531
|
|
|
498
532
|
|
|
533
|
+
# `--print` exits non-zero when CDX itself is unavailable, and that is a
|
|
534
|
+
# diagnostic that ran, not a companion that failed to launch. These are the
|
|
535
|
+
# codes that mean the program never got to run: the exec conventions for "found
|
|
536
|
+
# but not executable" and "not found". Death by signal is reported as a
|
|
537
|
+
# negative returncode and counts the same way.
|
|
538
|
+
_PROBE_LAUNCH_FAILURE_CODES = (126, 127)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def probe_command(executable):
|
|
542
|
+
"""How to ask the companion itself for its diagnostic output.
|
|
543
|
+
|
|
544
|
+
Not `launch_command`: on macOS that hands the bundle to `open`, which takes
|
|
545
|
+
`--print` for itself and reports nothing whatsoever about the companion. The
|
|
546
|
+
binary inside the bundle is what has to run, and an app bundle with no
|
|
547
|
+
runnable binary in it is exactly the case this gate exists to refuse.
|
|
548
|
+
"""
|
|
549
|
+
if platform.system() == "Darwin" and executable.endswith(".app"):
|
|
550
|
+
macos_dir = os.path.join(executable, "Contents", "MacOS")
|
|
551
|
+
try:
|
|
552
|
+
entries = sorted(os.listdir(macos_dir))
|
|
553
|
+
except OSError:
|
|
554
|
+
return None
|
|
555
|
+
for entry in entries:
|
|
556
|
+
candidate = os.path.join(macos_dir, entry)
|
|
557
|
+
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
|
558
|
+
return [candidate]
|
|
559
|
+
return None
|
|
560
|
+
return [executable]
|
|
561
|
+
|
|
562
|
+
|
|
499
563
|
def _probe(executable):
|
|
500
|
-
"""Does this companion
|
|
501
|
-
|
|
564
|
+
"""Does this companion actually run?
|
|
565
|
+
|
|
566
|
+
`--print` makes it emit its snapshot and exit, so a bundle that cannot
|
|
567
|
+
launch — wrong architecture, missing library, no binary inside it — is
|
|
568
|
+
refused here rather than after the working companion is gone.
|
|
569
|
+
"""
|
|
570
|
+
command = probe_command(executable)
|
|
571
|
+
if not command:
|
|
572
|
+
return False
|
|
502
573
|
try:
|
|
503
|
-
subprocess.run( # noqa: S603 (path comes from our own install state)
|
|
504
|
-
|
|
574
|
+
completed = subprocess.run( # noqa: S603 (path comes from our own install state)
|
|
575
|
+
command + ["--print"],
|
|
505
576
|
capture_output=True, timeout=20, check=False,
|
|
506
577
|
)
|
|
507
578
|
except (OSError, subprocess.SubprocessError):
|
|
508
579
|
return False
|
|
509
|
-
|
|
580
|
+
returncode = completed.returncode
|
|
581
|
+
return not (returncode < 0 or returncode in _PROBE_LAUNCH_FAILURE_CODES)
|
|
510
582
|
|
|
511
583
|
|
|
512
584
|
def interrupted_update(base_dir):
|