Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,28 @@ function Install-Raven([string]$UvPath, [string]$NodePath) {
Write-Warn "Found node but not npm; skipping TUI bundle build"
}
}
& $UvPath tool install --force -e $scriptDir
# Install all channel adapters by default; fall back to base raven if
# the umbrella extra fails to build on this platform, so one broken
# channel SDK cannot block the whole install.
try {
& $UvPath tool install --force -e "$scriptDir[channels]"
if ($LASTEXITCODE -ne 0) { throw "channel extras install failed" }
} catch {
Write-Warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
& $UvPath tool install --force -e "$scriptDir"
if ($LASTEXITCODE -ne 0) { Fail "Raven install failed." }
}
} else {
$wheelUrl = Resolve-RavenWheel
Write-Info " installing $wheelUrl"
& $UvPath tool install --force $wheelUrl
try {
& $UvPath tool install --force "raven[channels] @ $wheelUrl"
if ($LASTEXITCODE -ne 0) { throw "channel extras install failed" }
} catch {
Write-Warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
& $UvPath tool install --force $wheelUrl
if ($LASTEXITCODE -ne 0) { Fail "Raven install failed." }
}
}
& $UvPath tool update-shell | Out-Null
Write-Ok "Raven installed"
Expand Down
13 changes: 11 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,13 @@ install_raven() {
warn "No usable node found; skipping TUI build; raven tui may not work"
fi
fi
uv tool install --force -e "$script_dir"
# Install all channel adapters by default. If the umbrella extra fails to
# resolve/build on this platform, fall back to base raven so one broken
# channel SDK cannot block the whole install.
if ! uv tool install --force -e "$script_dir[channels]"; then
warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
uv tool install --force -e "$script_dir"
fi
else
# Remote mode: install the latest published release wheel, which bundles
# the prebuilt ui-tui/dist/entry.js (built by CI). We deliberately do NOT
Expand All @@ -185,7 +191,10 @@ install_raven() {
fi
[ -n "$wheel_url" ] || die "Could not resolve the latest raven release wheel from GitHub (check network, or set RAVEN_WHEEL_URL to a wheel URL)."
info " installing $wheel_url"
uv tool install --force "$wheel_url"
if ! uv tool install --force "raven[channels] @ $wheel_url"; then
warn "Channel dependencies failed to install; installed base raven only. Some channels stay unavailable (see: raven channels list)."
uv tool install --force "$wheel_url"
fi
fi
# Ensure ~/.local/bin (uv tool bin dir) is on PATH for future shells.
uv tool update-shell || true
Expand Down
32 changes: 30 additions & 2 deletions raven/channels/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from __future__ import annotations

import asyncio
import json
import sys
from importlib.metadata import PackageNotFoundError, distribution
from typing import Any

from loguru import logger
Expand All @@ -16,6 +19,31 @@
from raven.config.schema import Config


def _missing_dep_hint(modname: str) -> str:
"""How to install a channel's missing SDK, tailored to the install mode.

An editable (dev) checkout uses ``uv sync``; a wheel/tool install has no
source tree, so it must re-run the installer instead. PEP 610
``direct_url.json`` distinguishes them -- a wheel install records
``archive_info`` (no ``dir_info`` key), so ``.get`` chaining avoids a
KeyError when it is absent. This runs while a channel is already failing,
so a missing/corrupt file must degrade to the installer hint, never raise.
"""
editable = False
try:
raw = distribution("raven").read_text("direct_url.json")
if raw:
editable = bool(json.loads(raw).get("dir_info", {}).get("editable", False))
except (PackageNotFoundError, ValueError):
pass

if editable:
return f"Run: uv sync --extra channel-{modname}"
if sys.platform == "win32":
return "Re-run the installer to add channels: irm https://raven.evermind.ai/install.ps1 | iex"
return "Re-run the installer to add channels: curl -fsSL https://raven.evermind.ai/install.sh | bash"


class ChannelManager:
"""Manages chat channels: construct enabled adapters, start/stop, status."""

Expand Down Expand Up @@ -47,10 +75,10 @@ def _init_channels(self) -> None:
logger.info("{} channel enabled", spec.display_name)
except ImportError as e:
logger.warning(
"{} channel disabled: missing dependency ({}). Run: uv sync --extra channel-{}",
"{} channel disabled: missing dependency ({}). {}",
modname,
e,
modname,
_missing_dep_hint(modname),
)

self._validate_allow_from()
Expand Down
101 changes: 100 additions & 1 deletion tests/test_channels_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
status accessors. Outbound delivery moved to the spine outlets (no longer the
manager's job)."""

from importlib.metadata import PackageNotFoundError
from types import SimpleNamespace

import pytest

from raven.channels.contract import Capabilities, ChannelSpec
from raven.channels.manager import ChannelManager
from raven.channels.manager import ChannelManager, _missing_dep_hint


class _FakeChannel:
Expand Down Expand Up @@ -99,6 +100,104 @@ def test_validate_allow_from_rejects_empty(monkeypatch):
)


# ── _missing_dep_hint (install-mode / OS split) ───────────────────────

_EDITABLE_JSON = '{"url": "file:///src", "dir_info": {"editable": true}}'
_WHEEL_JSON = '{"url": "https://x/raven-0.1.2.whl", "archive_info": {}}'


def _patch_direct_url(monkeypatch, read_text_result):
class _Dist:
def read_text(self, name):
return read_text_result

monkeypatch.setattr("raven.channels.manager.distribution", lambda pkg: _Dist())


@pytest.mark.parametrize("modname", ["feishu", "weixin", "qq", "telegram", "dingtalk"])
def test_hint_editable_names_the_channel_extra(monkeypatch, modname):
"""Editable checkout -> `uv sync --extra channel-<name>`, name interpolated."""
_patch_direct_url(monkeypatch, _EDITABLE_JSON)
assert _missing_dep_hint(modname) == f"Run: uv sync --extra channel-{modname}"


@pytest.mark.parametrize(
"raw",
[
_WHEEL_JSON, # archive_info: no 'dir_info' key -> .get chain must not KeyError
None, # direct_url.json absent -> read_text returns None
'{"url": "file:///x", "dir_info": {}}', # dir_info present, 'editable' missing
"{}", # empty object
"{not valid json", # corrupt file -> JSONDecodeError must be swallowed
],
ids=["wheel", "absent", "dir_info_no_editable", "empty", "malformed"],
)
def test_hint_non_editable_points_to_installer(monkeypatch, raw):
"""Any non-editable / malformed direct_url.json -> installer hint, never raises."""
_patch_direct_url(monkeypatch, raw)
monkeypatch.setattr("raven.channels.manager.sys.platform", "linux")
hint = _missing_dep_hint("weixin")
assert "uv sync" not in hint
assert "install.sh" in hint


def test_hint_package_not_found_points_to_installer(monkeypatch):
"""raven distribution not found -> installer hint, no exception."""

def _raise(pkg):
raise PackageNotFoundError(pkg)

monkeypatch.setattr("raven.channels.manager.distribution", _raise)
monkeypatch.setattr("raven.channels.manager.sys.platform", "darwin")
assert "install.sh" in _missing_dep_hint("qq")


@pytest.mark.parametrize(
"platform, marker",
[("win32", "install.ps1"), ("darwin", "install.sh"), ("linux", "install.sh")],
)
def test_hint_installer_matches_os(monkeypatch, platform, marker):
"""Wheel install picks the installer for the running OS (irm vs curl)."""
_patch_direct_url(monkeypatch, _WHEEL_JSON)
monkeypatch.setattr("raven.channels.manager.sys.platform", platform)
assert marker in _missing_dep_hint("slack")


@pytest.mark.parametrize(
"direct_url, platform, expected",
[
(_EDITABLE_JSON, "linux", "uv sync --extra channel-feishu"),
(_WHEEL_JSON, "linux", "install.sh"),
(_WHEEL_JSON, "win32", "install.ps1"),
],
ids=["editable", "wheel-unix", "wheel-win"],
)
def test_init_warning_carries_install_hint(monkeypatch, direct_url, platform, expected):
"""A channel disabled by ImportError logs the mode-correct install hint."""
from loguru import logger

_patch_direct_url(monkeypatch, direct_url)
monkeypatch.setattr("raven.channels.manager.sys.platform", platform)

def boom(config):
raise ImportError("No module named 'lark_oapi'")

lines: list[str] = []
sink_id = logger.add(lambda m: lines.append(str(m)), level="WARNING")
try:
_manager(
monkeypatch,
{"feishu": _spec(boom)},
_config({"feishu": SimpleNamespace(enabled=True, allow_from=["*"])}),
)
finally:
logger.remove(sink_id)

warning = "".join(lines)
assert "feishu channel disabled" in warning
assert expected in warning


# ── status / accessors ────────────────────────────────────────────────


Expand Down
Loading