Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
753e4c4449
|
|||
|
f1734cd2c1
|
|||
|
22f39a1a77
|
|||
|
a9071abc9a
|
|||
|
edafd349df
|
+62
-32
@@ -6,7 +6,6 @@ import click
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import stat
|
||||
import shutil
|
||||
import re
|
||||
from importlib.metadata import PackageNotFoundError, version as package_version
|
||||
@@ -32,12 +31,28 @@ from browser_cli.client import (
|
||||
active_browser_targets,
|
||||
display_browser_name,
|
||||
save_remote_token,
|
||||
remote_target_for_alias,
|
||||
remote_browser_targets,
|
||||
)
|
||||
from browser_cli.platform import install_base_dir, is_windows
|
||||
from browser_cli.registry import load_registry
|
||||
|
||||
console = Console()
|
||||
|
||||
# Click's Group.shell_complete hardcodes no limit for get_short_help_str (defaults to 45 chars);
|
||||
# patch to use a wider limit so zsh completion descriptions aren't truncated.
|
||||
def _patched_group_shell_complete(self, ctx, incomplete):
|
||||
from click.shell_completion import CompletionItem
|
||||
results = [
|
||||
CompletionItem(name, help=command.get_short_help_str(limit=shutil.get_terminal_size().columns))
|
||||
for name, command in self.commands.items()
|
||||
if not command.hidden and name.startswith(incomplete)
|
||||
]
|
||||
results.extend(click.Command.shell_complete(self, ctx, incomplete))
|
||||
return results
|
||||
|
||||
click.Group.shell_complete = _patched_group_shell_complete
|
||||
|
||||
NATIVE_HOST_NAME = "com.browsercli.host"
|
||||
EXTENSION_ID = "bfpmkhngkjnfhabmfckgeohlilokodkg"
|
||||
|
||||
@@ -97,15 +112,25 @@ def _ensure_unique_browser_alias(alias: str, target_browser: str | None) -> None
|
||||
raise click.ClickException(f"Browser alias '{alias}' already exists")
|
||||
|
||||
|
||||
def _native_host_wrapper_path() -> Path:
|
||||
base_dir = install_base_dir()
|
||||
def _native_host_exe() -> Path:
|
||||
base = install_base_dir()
|
||||
if is_windows():
|
||||
return base_dir / "libexec" / "native-host.cmd"
|
||||
return base_dir / "libexec" / "native-host"
|
||||
return base / "libexec" / "browser-cli-native-host.cmd"
|
||||
return base / "libexec" / "browser-cli-native-host"
|
||||
|
||||
|
||||
def _native_host_script_path() -> Path:
|
||||
return _native_host_wrapper_path().with_name("native_host.py")
|
||||
def _write_native_host_exe(path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if is_windows():
|
||||
path.write_text(
|
||||
f'@echo off\r\n"{sys.executable}" -c "from browser_cli.native_host import main; main()" %*\r\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
else:
|
||||
path.write_text(
|
||||
f'#!{sys.executable}\nfrom browser_cli.native_host import main\nmain()\n'
|
||||
)
|
||||
path.chmod(path.stat().st_mode | 0o111)
|
||||
|
||||
|
||||
def _windows_registry_views():
|
||||
@@ -219,12 +244,34 @@ def clients_group(ctx):
|
||||
|
||||
all_clients = []
|
||||
|
||||
browser_alias = (ctx.obj or {}).get("browser")
|
||||
remote = (ctx.obj or {}).get("remote") or os.environ.get("BROWSER_CLI_REMOTE")
|
||||
if remote:
|
||||
token = (ctx.obj or {}).get("token") or os.environ.get("BROWSER_CLI_TOKEN")
|
||||
|
||||
if not remote and browser_alias:
|
||||
# --browser <host> without --remote: resolve host alias to a remote endpoint,
|
||||
# then show ALL clients from that remote (not just the one resolved profile).
|
||||
resolved = remote_target_for_alias(browser_alias)
|
||||
if resolved:
|
||||
resolved_token = token or resolved.token
|
||||
try:
|
||||
result = send_command("clients.list", profile=(ctx.obj or {}).get("browser"))
|
||||
targets = remote_browser_targets(resolved.remote, resolved_token)
|
||||
except (BrowserNotConnected, RuntimeError) as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
sys.exit(1)
|
||||
for target in targets:
|
||||
try:
|
||||
result = send_command("clients.list", profile=target.profile, remote=resolved.remote, token=resolved_token)
|
||||
for c in (result or []):
|
||||
c["profile"] = c.get("profile") or (ctx.obj or {}).get("browser") or "remote"
|
||||
c["profile"] = target.display_name
|
||||
all_clients.append(c)
|
||||
except (BrowserNotConnected, RuntimeError):
|
||||
continue
|
||||
elif remote:
|
||||
try:
|
||||
result = send_command("clients.list", profile=browser_alias, remote=remote, token=token)
|
||||
for c in (result or []):
|
||||
c["profile"] = c.get("profile") or browser_alias or "remote"
|
||||
all_clients.append(c)
|
||||
except (BrowserNotConnected, RuntimeError) as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
@@ -308,22 +355,8 @@ def cmd_clients_rename(target_browser, alias):
|
||||
def cmd_install(browser):
|
||||
"""Register the native messaging host and print extension load instructions."""
|
||||
|
||||
# Install wrapper outside PATH — the browser uses the absolute path from the
|
||||
# native messaging manifest, so only `browser-cli` needs to be on PATH.
|
||||
wrapper_path = _native_host_wrapper_path()
|
||||
native_host_script_path = _native_host_script_path()
|
||||
wrapper_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(Path(__file__).with_name("native_host.py"), native_host_script_path)
|
||||
if not is_windows():
|
||||
native_host_script_path.chmod(
|
||||
native_host_script_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
||||
)
|
||||
wrapper_content = f'#!/bin/sh\nexec "{sys.executable}" "{native_host_script_path}" "$@"\n'
|
||||
wrapper_path.write_text(wrapper_content)
|
||||
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
||||
else:
|
||||
wrapper_content = f'@echo off\r\n"{sys.executable}" "{native_host_script_path}" %*\r\n'
|
||||
wrapper_path.write_text(wrapper_content, encoding="utf-8")
|
||||
host_exe = _native_host_exe()
|
||||
_write_native_host_exe(host_exe)
|
||||
|
||||
# Load extension
|
||||
ext_urls = {
|
||||
@@ -346,14 +379,14 @@ def cmd_install(browser):
|
||||
manifest = {
|
||||
"name": NATIVE_HOST_NAME,
|
||||
"description": "browser-cli native messaging host",
|
||||
"path": str(wrapper_path),
|
||||
"path": str(host_exe),
|
||||
"type": "stdio",
|
||||
"allowed_origins": [f"chrome-extension://{extension_id}/"],
|
||||
}
|
||||
|
||||
installed = []
|
||||
if is_windows():
|
||||
manifest_dir = wrapper_path.parent
|
||||
manifest_dir = host_exe.parent
|
||||
manifest_dir.mkdir(parents=True, exist_ok=True)
|
||||
manifest_path = manifest_dir / f"{NATIVE_HOST_NAME}.json"
|
||||
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
||||
@@ -380,10 +413,7 @@ def cmd_install(browser):
|
||||
console.print(f"[green]✓[/green] Registered native host: {p}")
|
||||
else:
|
||||
console.print(f"[green]✓[/green] Wrote native host manifest: {p}")
|
||||
console.print(f"[green]✓[/green] Installed native host script: {native_host_script_path}")
|
||||
console.print(f"[green]✓[/green] Installed native host wrapper: {wrapper_path}")
|
||||
if is_windows():
|
||||
console.print("\n[green]✓[/green] Wrote native host manifest:", manifest_path)
|
||||
console.print(f"[green]✓[/green] Installed native host: {host_exe}")
|
||||
|
||||
console.print(f"\n[bold]Step 2:[/bold] Restart {browser.capitalize()} completely (quit app, then reopen)")
|
||||
console.print("\n[green bold]✓ Installation complete![/green bold]")
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "com.browsercli.host",
|
||||
"description": "browser-cli native messaging host",
|
||||
"path": "/REPLACE_WITH_ABSOLUTE_PATH/browser-cli/libexec/native-host",
|
||||
"type": "stdio",
|
||||
"allowed_origins": [
|
||||
"chrome-extension://REPLACE_WITH_EXTENSION_ID/"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "browser-cli",
|
||||
"version": "0.8.4",
|
||||
"version": "0.8.6",
|
||||
"description": "Control your browser from the terminal via browser-cli",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "browser-cli"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
description = "Control your real running browser from the terminal via a browser extension"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
|
||||
@@ -8,7 +8,7 @@ pkgs.mkShell {
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "browser-cli dev shell: node $(node --version), npm $(npm --version), uv $(uv --version)"
|
||||
echo "browser-cli dev shell: node $(node --version), npm $(npm --version), uv $(uv --version | awk '{print $2}')"
|
||||
|
||||
if [ -f package-lock.json ]; then
|
||||
if [ ! -f node_modules/.package-lock.json ] || [ package-lock.json -nt node_modules/.package-lock.json ]; then
|
||||
|
||||
@@ -286,7 +286,6 @@ class TestTabs:
|
||||
def test_tabs_filter_predicate(self, b, mock_send):
|
||||
mock_send.return_value = [TAB_DATA, {**TAB_DATA, "id": 11, "url": "https://youtube.com"}]
|
||||
tabs = b.tabs_filter(lambda tab: "youtube" in tab.url)
|
||||
print(tabs)
|
||||
assert [tab.id for tab in tabs] == [11]
|
||||
|
||||
def test_tabs_filter_list_transformer(self, b, mock_send):
|
||||
|
||||
+74
-34
@@ -80,22 +80,15 @@ def test_install_help_lists_supported_browsers():
|
||||
assert "[chrome|chromium|brave|edge|vivaldi]" in result.output
|
||||
|
||||
|
||||
def test_install_windows_registers_native_host(tmp_path, monkeypatch):
|
||||
local_app_data = tmp_path / "LocalAppData"
|
||||
extension_dir = tmp_path / "extension"
|
||||
extension_dir.mkdir()
|
||||
native_host_src = tmp_path / "native_host.py"
|
||||
native_host_src.write_text("print('ok')", encoding="utf-8")
|
||||
def test_install_windows_registers_native_host(tmp_path):
|
||||
writes = []
|
||||
|
||||
class FakeKey:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
def __exit__(self, _exc_type, _exc, _tb):
|
||||
return False
|
||||
|
||||
fake_winreg = SimpleNamespace(
|
||||
@@ -104,35 +97,47 @@ def test_install_windows_registers_native_host(tmp_path, monkeypatch):
|
||||
KEY_WOW64_32KEY=0x0200,
|
||||
KEY_WOW64_64KEY=0x0100,
|
||||
REG_SZ=1,
|
||||
CreateKeyEx=lambda _root, path, _reserved, _access: FakeKey(path),
|
||||
SetValueEx=lambda key, name, _reserved, _reg_type, value: writes.append((key.path, name, value)),
|
||||
)
|
||||
|
||||
def fake_create_key(root, path, reserved, access):
|
||||
return FakeKey(path)
|
||||
|
||||
def fake_set_value(key, name, reserved, reg_type, value):
|
||||
writes.append((key.path, name, value))
|
||||
|
||||
fake_winreg.CreateKeyEx = fake_create_key
|
||||
fake_winreg.SetValueEx = fake_set_value
|
||||
|
||||
monkeypatch.setenv("LOCALAPPDATA", str(local_app_data))
|
||||
|
||||
host_exe = tmp_path / "browser-cli-native-host.exe"
|
||||
with patch("browser_cli.cli.is_windows", return_value=True), patch(
|
||||
"browser_cli.cli.Path.home", return_value=tmp_path
|
||||
), patch("browser_cli.cli.click.prompt", return_value="abc123"), patch(
|
||||
"browser_cli.cli.shutil.copy2"
|
||||
) as copy2, patch("browser_cli.cli.Path.write_text") as write_text, patch.dict(
|
||||
sys.modules, {"winreg": fake_winreg}
|
||||
):
|
||||
copy2.side_effect = lambda src, dst: Path(dst).write_text(native_host_src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
"browser_cli.cli._native_host_exe", return_value=host_exe
|
||||
), patch("browser_cli.cli._write_native_host_exe"), patch(
|
||||
"browser_cli.cli.Path.write_text"
|
||||
), patch.dict(sys.modules, {"winreg": fake_winreg}):
|
||||
result = CliRunner().invoke(main, ["install", "edge"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert any("Software\\Microsoft\\Edge\\NativeMessagingHosts\\com.browsercli.host" in path for path, _, _ in writes)
|
||||
assert "Registered native host" in result.output
|
||||
assert "Wrote native host manifest" in result.output
|
||||
wrapper_writes = [call.args[0] for call in write_text.call_args_list if call.args]
|
||||
assert any("@echo off" in text for text in wrapper_writes)
|
||||
|
||||
def test_write_native_host_exe_unix(tmp_path):
|
||||
from browser_cli.cli import _write_native_host_exe
|
||||
|
||||
host = tmp_path / "libexec" / "browser-cli-native-host"
|
||||
with patch("browser_cli.cli.is_windows", return_value=False):
|
||||
_write_native_host_exe(host)
|
||||
|
||||
assert host.exists()
|
||||
content = host.read_text()
|
||||
assert content.startswith(f"#!{sys.executable}")
|
||||
assert "from browser_cli.native_host import main" in content
|
||||
assert host.stat().st_mode & 0o111 # executable bit set
|
||||
|
||||
|
||||
def test_write_native_host_exe_windows(tmp_path):
|
||||
from browser_cli.cli import _write_native_host_exe
|
||||
|
||||
host = tmp_path / "libexec" / "browser-cli-native-host.cmd"
|
||||
with patch("browser_cli.cli.is_windows", return_value=True):
|
||||
_write_native_host_exe(host)
|
||||
|
||||
assert host.exists()
|
||||
content = host.read_text(encoding="utf-8")
|
||||
assert "@echo off" in content
|
||||
assert "browser_cli.native_host" in content
|
||||
|
||||
|
||||
def test_clients_exits_cleanly_when_registry_is_missing():
|
||||
with patch("browser_cli.cli.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")), patch(
|
||||
@@ -163,9 +168,11 @@ def test_clients_reads_registry_with_trailing_garbage(tmp_path):
|
||||
assert "0.8.2" in result.output
|
||||
|
||||
def test_clients_remote_uses_remote_endpoint_without_local_registry():
|
||||
def fake_send_command(command, args=None, profile=None):
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None):
|
||||
assert command == "clients.list"
|
||||
assert profile is None
|
||||
assert remote == "127.0.0.1:8765"
|
||||
assert token == "test"
|
||||
return [{"name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}]
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True), patch(
|
||||
@@ -187,7 +194,41 @@ def test_clients_remote_respects_global_browser_route():
|
||||
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
send_command.assert_called_once_with("clients.list", profile="work")
|
||||
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", token=None)
|
||||
|
||||
|
||||
def test_clients_browser_alias_resolves_to_remote():
|
||||
"""--browser <host> without --remote resolves the alias, fetches all targets from that remote,
|
||||
and shows only clients from that host (not local profiles)."""
|
||||
from browser_cli.client import BrowserTarget
|
||||
|
||||
resolved_target = BrowserTarget(
|
||||
profile="automatisation",
|
||||
display_name="192.168.188.104:automatisation",
|
||||
socket_path="",
|
||||
remote="192.168.188.104:8765",
|
||||
token="tok",
|
||||
)
|
||||
all_remote_targets = [resolved_target]
|
||||
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None):
|
||||
assert command == "clients.list"
|
||||
assert profile == "automatisation"
|
||||
assert remote == "192.168.188.104:8765"
|
||||
assert token == "tok"
|
||||
return [{"name": "Chrome", "version": "147.0.0.0", "extensionVersion": "0.8.5"}]
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True), patch(
|
||||
"browser_cli.cli.remote_target_for_alias", return_value=resolved_target
|
||||
), patch(
|
||||
"browser_cli.cli.remote_browser_targets", return_value=all_remote_targets
|
||||
), patch("browser_cli.cli.send_command", side_effect=fake_send_command) as send_command:
|
||||
result = CliRunner().invoke(main, ["--browser", "192.168.188.104", "clients"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_called_once()
|
||||
assert "Chrome" in result.output
|
||||
assert "0.8.5" in result.output
|
||||
|
||||
|
||||
def test_clients_shows_named_profile_and_uses_socket_uuid_for_default(tmp_path):
|
||||
@@ -357,7 +398,6 @@ def test_groups_move_accepts_left_short_alias():
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_windows_list_multi_browser_shows_browser_column():
|
||||
def fake_send_command(command, args=None, profile=None):
|
||||
assert command == "windows.list"
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Tests for dom.* commands (require an http/https active tab)."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_dom_query_body(browser, http_tab):
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Tests for extract.* commands (require an http/https active tab)."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_extract_links(browser, http_tab):
|
||||
@@ -58,15 +56,3 @@ def test_extract_markdown_missing_selector_errors(browser, http_tab):
|
||||
assert "No element" in str(exc)
|
||||
|
||||
|
||||
def test_dom_exists(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
result = browser("dom.exists", {"selector": "body"})
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_dom_query(browser, http_tab):
|
||||
browser("tabs.active", {"tabId": http_tab["id"]})
|
||||
elements = browser("dom.query", {"selector": "body"})
|
||||
assert isinstance(elements, list)
|
||||
assert len(elements) > 0
|
||||
assert elements[0].get("tag") == "body"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for group.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_group_list(browser):
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Tests for navigate.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_nav_open_and_close(browser):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for session.* commands."""
|
||||
import time
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
from tests.conftest import TEST_BROWSER_PROFILE
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for tabs.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_tabs_list(browser):
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""Tests for windows.* commands."""
|
||||
import pytest
|
||||
from browser_cli.client import send_command
|
||||
|
||||
|
||||
def test_windows_list(browser):
|
||||
|
||||
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "browser-cli"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
@@ -27,14 +27,14 @@ dev = [{ name = "pytest", specifier = ">=8" }]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.2"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -90,11 +90,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -135,15 +135,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.3.3"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user