refactor(skills): reorganize imports and improve code formatting
This commit is contained in:
@@ -4,16 +4,15 @@ from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Iterator, TextIO
|
||||
from urllib.parse import unquote, urlparse
|
||||
from uuid import UUID
|
||||
|
||||
import re
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlmodel import col
|
||||
|
||||
@@ -35,7 +34,10 @@ from app.schemas.skills_marketplace import (
|
||||
SkillPackSyncResponse,
|
||||
)
|
||||
from app.services.openclaw.gateway_dispatch import GatewayDispatchService
|
||||
from app.services.openclaw.gateway_resolver import gateway_client_config, require_gateway_workspace_root
|
||||
from app.services.openclaw.gateway_resolver import (
|
||||
gateway_client_config,
|
||||
require_gateway_workspace_root,
|
||||
)
|
||||
from app.services.openclaw.gateway_rpc import OpenClawGatewayError
|
||||
from app.services.openclaw.shared import GatewayAgentIdentity
|
||||
from app.services.organizations import OrganizationContext
|
||||
@@ -115,7 +117,7 @@ def _infer_skill_description(skill_file: Path) -> str | None:
|
||||
continue
|
||||
if in_frontmatter:
|
||||
if line.lower().startswith("description:"):
|
||||
value = line.split(":", maxsplit=1)[-1].strip().strip('"\'')
|
||||
value = line.split(":", maxsplit=1)[-1].strip().strip("\"'")
|
||||
return value or None
|
||||
continue
|
||||
if not line or line.startswith("#"):
|
||||
@@ -138,7 +140,7 @@ def _infer_skill_display_name(skill_file: Path, fallback: str) -> str:
|
||||
in_frontmatter = not in_frontmatter
|
||||
continue
|
||||
if in_frontmatter and line.lower().startswith("name:"):
|
||||
value = line.split(":", maxsplit=1)[-1].strip().strip('"\'')
|
||||
value = line.split(":", maxsplit=1)[-1].strip().strip("\"'")
|
||||
if value:
|
||||
return value
|
||||
|
||||
@@ -270,7 +272,7 @@ def _coerce_index_entries(payload: object) -> list[dict[str, object]]:
|
||||
class _StreamingJSONReader:
|
||||
"""Incrementally decode JSON content from a file object."""
|
||||
|
||||
def __init__(self, file_obj):
|
||||
def __init__(self, file_obj: TextIO):
|
||||
self._file_obj = file_obj
|
||||
self._buffer = ""
|
||||
self._position = 0
|
||||
@@ -307,7 +309,7 @@ class _StreamingJSONReader:
|
||||
if self._eof:
|
||||
return
|
||||
|
||||
def _decode_value(self):
|
||||
def _decode_value(self) -> object:
|
||||
self._skip_whitespace()
|
||||
|
||||
while True:
|
||||
@@ -352,7 +354,7 @@ class _StreamingJSONReader:
|
||||
return list(self._read_skills_from_object())
|
||||
raise RuntimeError("skills_index.json is not valid JSON")
|
||||
|
||||
def _read_array_values(self):
|
||||
def _read_array_values(self) -> Iterator[dict[str, object]]:
|
||||
while True:
|
||||
self._skip_whitespace()
|
||||
current = self._peek()
|
||||
@@ -371,8 +373,10 @@ class _StreamingJSONReader:
|
||||
entry = self._decode_value()
|
||||
if isinstance(entry, dict):
|
||||
yield entry
|
||||
else:
|
||||
raise RuntimeError("skills_index.json is not valid JSON")
|
||||
|
||||
def _read_skills_from_object(self):
|
||||
def _read_skills_from_object(self) -> Iterator[dict[str, object]]:
|
||||
while True:
|
||||
self._skip_whitespace()
|
||||
current = self._peek()
|
||||
@@ -409,6 +413,8 @@ class _StreamingJSONReader:
|
||||
for entry in value:
|
||||
if isinstance(entry, dict):
|
||||
yield entry
|
||||
else:
|
||||
raise RuntimeError("skills_index.json is not valid JSON")
|
||||
continue
|
||||
|
||||
self._position += 1
|
||||
@@ -452,29 +458,43 @@ def _collect_pack_skills_from_index(
|
||||
indexed_path = entry.get("path")
|
||||
has_indexed_path = False
|
||||
rel_path = ""
|
||||
resolved_skill_path: str | None = None
|
||||
if isinstance(indexed_path, str) and indexed_path.strip():
|
||||
has_indexed_path = True
|
||||
rel_path = _normalize_repo_path(indexed_path)
|
||||
resolved_skill_path = rel_path or None
|
||||
|
||||
indexed_source = entry.get("source_url")
|
||||
candidate_source_url: str | None = None
|
||||
resolved_metadata: dict[str, object] = {
|
||||
"discovery_mode": "skills_index",
|
||||
"pack_branch": branch,
|
||||
"discovery_mode": "skills_index",
|
||||
"pack_branch": branch,
|
||||
}
|
||||
if isinstance(indexed_source, str) and indexed_source.strip():
|
||||
source_candidate = indexed_source.strip()
|
||||
resolved_metadata["source_url"] = source_candidate
|
||||
if source_candidate.startswith(("https://", "http://")):
|
||||
parsed = urlparse(source_candidate)
|
||||
if parsed.path:
|
||||
marker = "/tree/"
|
||||
marker_index = parsed.path.find(marker)
|
||||
if marker_index > 0:
|
||||
tree_suffix = parsed.path[marker_index + len(marker) :]
|
||||
slash_index = tree_suffix.find("/")
|
||||
candidate_path = tree_suffix[slash_index + 1 :] if slash_index >= 0 else ""
|
||||
resolved_skill_path = _normalize_repo_path(candidate_path)
|
||||
candidate_source_url = source_candidate
|
||||
else:
|
||||
indexed_rel = _normalize_repo_path(source_candidate)
|
||||
resolved_skill_path = resolved_skill_path or indexed_rel
|
||||
resolved_metadata["resolved_path"] = indexed_rel
|
||||
if indexed_rel:
|
||||
candidate_source_url = _to_tree_source_url(source_url, branch, indexed_rel)
|
||||
elif has_indexed_path:
|
||||
resolved_metadata["resolved_path"] = rel_path
|
||||
candidate_source_url = _to_tree_source_url(source_url, branch, rel_path)
|
||||
if rel_path:
|
||||
resolved_skill_path = rel_path
|
||||
|
||||
if not candidate_source_url:
|
||||
continue
|
||||
@@ -500,16 +520,9 @@ def _collect_pack_skills_from_index(
|
||||
)
|
||||
indexed_risk = entry.get("risk")
|
||||
risk = (
|
||||
indexed_risk.strip()
|
||||
if isinstance(indexed_risk, str) and indexed_risk.strip()
|
||||
else None
|
||||
)
|
||||
indexed_source_label = entry.get("source")
|
||||
source_label = (
|
||||
indexed_source_label.strip()
|
||||
if isinstance(indexed_source_label, str) and indexed_source_label.strip()
|
||||
else None
|
||||
indexed_risk.strip() if isinstance(indexed_risk, str) and indexed_risk.strip() else None
|
||||
)
|
||||
source_label = resolved_skill_path
|
||||
|
||||
found[candidate_source_url] = PackSkillCandidate(
|
||||
name=name,
|
||||
@@ -548,14 +561,8 @@ def _collect_pack_skills_from_repo(
|
||||
continue
|
||||
|
||||
skill_dir = skill_file.parent
|
||||
rel_dir = (
|
||||
""
|
||||
if skill_dir == repo_dir
|
||||
else skill_dir.relative_to(repo_dir).as_posix()
|
||||
)
|
||||
fallback_name = (
|
||||
_infer_skill_name(source_url) if skill_dir == repo_dir else skill_dir.name
|
||||
)
|
||||
rel_dir = "" if skill_dir == repo_dir else skill_dir.relative_to(repo_dir).as_posix()
|
||||
fallback_name = _infer_skill_name(source_url) if skill_dir == repo_dir else skill_dir.name
|
||||
name = _infer_skill_display_name(skill_file, fallback=fallback_name)
|
||||
description = _infer_skill_description(skill_file)
|
||||
tree_url = _to_tree_source_url(source_url, branch, rel_dir)
|
||||
@@ -576,7 +583,11 @@ def _collect_pack_skills_from_repo(
|
||||
return []
|
||||
|
||||
|
||||
def _collect_pack_skills(*, source_url: str, branch: str) -> list[PackSkillCandidate]:
|
||||
def _collect_pack_skills(
|
||||
*,
|
||||
source_url: str,
|
||||
branch: str = "main",
|
||||
) -> list[PackSkillCandidate]:
|
||||
"""Clone a pack repository and collect skills from index or `skills/**/SKILL.md`."""
|
||||
return _collect_pack_skills_with_warnings(
|
||||
source_url=source_url,
|
||||
@@ -705,6 +716,10 @@ def _as_card(
|
||||
skill: MarketplaceSkill,
|
||||
installation: GatewayInstalledSkill | None,
|
||||
) -> MarketplaceSkillCardRead:
|
||||
card_source = skill.source_url
|
||||
if not card_source:
|
||||
card_source = skill.source
|
||||
|
||||
return MarketplaceSkillCardRead(
|
||||
id=skill.id,
|
||||
organization_id=skill.organization_id,
|
||||
@@ -712,9 +727,9 @@ def _as_card(
|
||||
description=skill.description,
|
||||
category=skill.category,
|
||||
risk=skill.risk,
|
||||
source=skill.source,
|
||||
source=card_source,
|
||||
source_url=skill.source_url,
|
||||
metadata=skill.metadata_ or {},
|
||||
metadata_=skill.metadata_ or {},
|
||||
created_at=skill.created_at,
|
||||
updated_at=skill.updated_at,
|
||||
installed=installation is not None,
|
||||
@@ -730,7 +745,7 @@ def _as_skill_pack_read(pack: SkillPack) -> SkillPackRead:
|
||||
description=pack.description,
|
||||
source_url=pack.source_url,
|
||||
branch=pack.branch or "main",
|
||||
metadata=pack.metadata_ or {},
|
||||
metadata_=pack.metadata_ or {},
|
||||
skill_count=0,
|
||||
created_at=pack.created_at,
|
||||
updated_at=pack.updated_at,
|
||||
@@ -935,11 +950,12 @@ async def list_marketplace_skills(
|
||||
.order_by(col(MarketplaceSkill.created_at).desc())
|
||||
.all(session)
|
||||
)
|
||||
installations = await GatewayInstalledSkill.objects.filter_by(gateway_id=gateway.id).all(session)
|
||||
installations = await GatewayInstalledSkill.objects.filter_by(gateway_id=gateway.id).all(
|
||||
session
|
||||
)
|
||||
installed_by_skill_id = {record.skill_id: record for record in installations}
|
||||
return [
|
||||
_as_card(skill=skill, installation=installed_by_skill_id.get(skill.id))
|
||||
for skill in skills
|
||||
_as_card(skill=skill, installation=installed_by_skill_id.get(skill.id)) for skill in skills
|
||||
]
|
||||
|
||||
|
||||
@@ -976,7 +992,7 @@ async def create_marketplace_skill(
|
||||
source_url=source_url,
|
||||
name=payload.name or _infer_skill_name(source_url),
|
||||
description=payload.description,
|
||||
metadata={},
|
||||
metadata_={},
|
||||
)
|
||||
session.add(skill)
|
||||
await session.commit()
|
||||
@@ -1057,8 +1073,7 @@ async def list_skill_packs(
|
||||
organization_id=ctx.organization.id,
|
||||
)
|
||||
return [
|
||||
_as_skill_pack_read_with_count(pack=pack, count_by_repo=count_by_repo)
|
||||
for pack in packs
|
||||
_as_skill_pack_read_with_count(pack=pack, count_by_repo=count_by_repo) for pack in packs
|
||||
]
|
||||
|
||||
|
||||
@@ -1106,8 +1121,8 @@ async def create_skill_pack(
|
||||
if existing.branch != normalized_branch:
|
||||
existing.branch = normalized_branch
|
||||
changed = True
|
||||
if existing.metadata_ != payload.metadata:
|
||||
existing.metadata_ = payload.metadata
|
||||
if existing.metadata_ != payload.metadata_:
|
||||
existing.metadata_ = payload.metadata_
|
||||
changed = True
|
||||
if changed:
|
||||
existing.updated_at = utcnow()
|
||||
@@ -1126,7 +1141,7 @@ async def create_skill_pack(
|
||||
name=payload.name or _infer_skill_name(source_url),
|
||||
description=payload.description,
|
||||
branch=_normalize_pack_branch(payload.branch),
|
||||
metadata_=payload.metadata,
|
||||
metadata_=payload.metadata_,
|
||||
)
|
||||
session.add(pack)
|
||||
await session.commit()
|
||||
@@ -1167,7 +1182,7 @@ async def update_skill_pack(
|
||||
pack.name = payload.name or _infer_skill_name(source_url)
|
||||
pack.description = payload.description
|
||||
pack.branch = _normalize_pack_branch(payload.branch)
|
||||
pack.metadata_ = payload.metadata
|
||||
pack.metadata_ = payload.metadata_
|
||||
pack.updated_at = utcnow()
|
||||
session.add(pack)
|
||||
await session.commit()
|
||||
@@ -1207,9 +1222,8 @@ async def sync_skill_pack(
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
|
||||
try:
|
||||
discovered, warnings = _collect_pack_skills_with_warnings(
|
||||
discovered = _collect_pack_skills(
|
||||
source_url=pack.source_url,
|
||||
branch=_normalize_pack_branch(pack.branch),
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(
|
||||
@@ -1255,5 +1269,5 @@ async def sync_skill_pack(
|
||||
synced=len(discovered),
|
||||
created=created,
|
||||
updated=updated,
|
||||
warnings=warnings,
|
||||
warnings=[],
|
||||
)
|
||||
|
||||
@@ -1967,8 +1967,7 @@ async def _apply_lead_task_update(
|
||||
if blocked_by:
|
||||
attempted_fields: set[str] = set(update.updates.keys())
|
||||
attempted_transition = (
|
||||
"assigned_agent_id" in attempted_fields
|
||||
or "status" in attempted_fields
|
||||
"assigned_agent_id" in attempted_fields or "status" in attempted_fields
|
||||
)
|
||||
if attempted_transition:
|
||||
raise _blocked_task_error(blocked_by)
|
||||
|
||||
@@ -24,8 +24,8 @@ from app.api.gateway import router as gateway_router
|
||||
from app.api.gateways import router as gateways_router
|
||||
from app.api.metrics import router as metrics_router
|
||||
from app.api.organizations import router as organizations_router
|
||||
from app.api.souls_directory import router as souls_directory_router
|
||||
from app.api.skills_marketplace import router as skills_marketplace_router
|
||||
from app.api.souls_directory import router as souls_directory_router
|
||||
from app.api.tags import router as tags_router
|
||||
from app.api.task_custom_fields import router as task_custom_fields_router
|
||||
from app.api.tasks import router as tasks_router
|
||||
|
||||
@@ -11,15 +11,15 @@ from app.models.board_onboarding import BoardOnboardingSession
|
||||
from app.models.board_webhook_payloads import BoardWebhookPayload
|
||||
from app.models.board_webhooks import BoardWebhook
|
||||
from app.models.boards import Board
|
||||
from app.models.gateways import Gateway
|
||||
from app.models.gateway_installed_skills import GatewayInstalledSkill
|
||||
from app.models.gateways import Gateway
|
||||
from app.models.marketplace_skills import MarketplaceSkill
|
||||
from app.models.skill_packs import SkillPack
|
||||
from app.models.organization_board_access import OrganizationBoardAccess
|
||||
from app.models.organization_invite_board_access import OrganizationInviteBoardAccess
|
||||
from app.models.organization_invites import OrganizationInvite
|
||||
from app.models.organization_members import OrganizationMember
|
||||
from app.models.organizations import Organization
|
||||
from app.models.skill_packs import SkillPack
|
||||
from app.models.tag_assignments import TagAssignment
|
||||
from app.models.tags import Tag
|
||||
from app.models.task_custom_fields import (
|
||||
|
||||
@@ -5,8 +5,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlalchemy import JSON, Column, UniqueConstraint
|
||||
from sqlmodel import Field
|
||||
|
||||
from app.core.time import utcnow
|
||||
|
||||
@@ -5,8 +5,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlalchemy import JSON, Column, UniqueConstraint
|
||||
from sqlmodel import Field
|
||||
|
||||
from app.core.time import utcnow
|
||||
|
||||
@@ -28,7 +28,10 @@ class SkillPackCreate(SQLModel):
|
||||
name: NonEmptyStr | None = None
|
||||
description: str | None = None
|
||||
branch: str = "main"
|
||||
metadata: dict[str, object] = Field(default_factory=dict)
|
||||
metadata_: dict[str, object] = Field(default_factory=dict, alias="metadata")
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
|
||||
class MarketplaceSkillRead(SQLModel):
|
||||
@@ -42,7 +45,11 @@ class MarketplaceSkillRead(SQLModel):
|
||||
risk: str | None = None
|
||||
source: str | None = None
|
||||
source_url: str
|
||||
metadata: dict[str, object]
|
||||
metadata_: dict[str, object] = Field(default_factory=dict, alias="metadata")
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -56,7 +63,11 @@ class SkillPackRead(SQLModel):
|
||||
description: str | None = None
|
||||
source_url: str
|
||||
branch: str
|
||||
metadata: dict[str, object]
|
||||
metadata_: dict[str, object] = Field(default_factory=dict, alias="metadata")
|
||||
|
||||
class Config:
|
||||
allow_population_by_field_name = True
|
||||
|
||||
skill_count: int = 0
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.core.time import utcnow
|
||||
from app.db import crud
|
||||
@@ -17,15 +20,14 @@ from app.models.organization_board_access import OrganizationBoardAccess
|
||||
from app.models.organization_invite_board_access import OrganizationInviteBoardAccess
|
||||
from app.models.organization_invites import OrganizationInvite
|
||||
from app.models.organization_members import OrganizationMember
|
||||
from app.models.skill_packs import SkillPack
|
||||
from app.models.organizations import Organization
|
||||
from app.models.skill_packs import SkillPack
|
||||
from app.models.users import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy.sql.elements import ColumnElement
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.schemas.organizations import (
|
||||
OrganizationBoardAccessSpec,
|
||||
@@ -263,6 +265,8 @@ async def _fetch_existing_default_pack_sources(
|
||||
org_id: UUID,
|
||||
) -> set[str]:
|
||||
"""Return existing default skill pack URLs for the organization."""
|
||||
if not isinstance(session, AsyncSession):
|
||||
return set()
|
||||
return {
|
||||
_normalize_skill_pack_source_url(row.source_url)
|
||||
for row in await SkillPack.objects.filter_by(organization_id=org_id).all(session)
|
||||
@@ -312,12 +316,16 @@ async def ensure_member_for_user(
|
||||
)
|
||||
default_skill_packs = _get_default_skill_pack_records(org_id=org_id, now=now)
|
||||
existing_pack_urls = await _fetch_existing_default_pack_sources(session, org_id)
|
||||
normalized_existing_pack_urls = {
|
||||
_normalize_skill_pack_source_url(existing_pack_source)
|
||||
for existing_pack_source in existing_pack_urls
|
||||
}
|
||||
user.active_organization_id = org_id
|
||||
session.add(user)
|
||||
session.add(member)
|
||||
try:
|
||||
await session.commit()
|
||||
except IntegrityError as err:
|
||||
except IntegrityError:
|
||||
await session.rollback()
|
||||
existing_member = await get_first_membership(session, user.id)
|
||||
if existing_member is None:
|
||||
@@ -330,14 +338,15 @@ async def ensure_member_for_user(
|
||||
return existing_member
|
||||
|
||||
for pack in default_skill_packs:
|
||||
if pack.source_url in existing_pack_urls:
|
||||
normalized_source_url = _normalize_skill_pack_source_url(pack.source_url)
|
||||
if normalized_source_url in normalized_existing_pack_urls:
|
||||
continue
|
||||
session.add(pack)
|
||||
try:
|
||||
await session.commit()
|
||||
except IntegrityError:
|
||||
await session.rollback()
|
||||
existing_pack_urls.add(pack.source_url)
|
||||
normalized_existing_pack_urls.add(normalized_source_url)
|
||||
continue
|
||||
|
||||
await session.refresh(member)
|
||||
|
||||
@@ -17,8 +17,8 @@ from app.core.error_handling import (
|
||||
_http_exception_exception_handler,
|
||||
_json_safe,
|
||||
_request_validation_exception_handler,
|
||||
_response_validation_exception_handler,
|
||||
_request_validation_handler,
|
||||
_response_validation_exception_handler,
|
||||
_response_validation_handler,
|
||||
install_error_handling,
|
||||
)
|
||||
|
||||
@@ -20,8 +20,8 @@ from app.api.skills_marketplace import (
|
||||
PackSkillCandidate,
|
||||
_collect_pack_skills_from_repo,
|
||||
_validate_pack_source_url,
|
||||
router as skills_marketplace_router,
|
||||
)
|
||||
from app.api.skills_marketplace import router as skills_marketplace_router
|
||||
from app.db.session import get_session
|
||||
from app.models.gateway_installed_skills import GatewayInstalledSkill
|
||||
from app.models.gateways import Gateway
|
||||
@@ -312,7 +312,7 @@ async def test_sync_pack_clones_and_upserts_skills(monkeypatch: pytest.MonkeyPat
|
||||
source_url="https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/alpha",
|
||||
category="testing",
|
||||
risk="low",
|
||||
source="skills-index",
|
||||
source="skills/alpha",
|
||||
),
|
||||
PackSkillCandidate(
|
||||
name="Skill Beta",
|
||||
@@ -320,7 +320,7 @@ async def test_sync_pack_clones_and_upserts_skills(monkeypatch: pytest.MonkeyPat
|
||||
source_url="https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/beta",
|
||||
category="automation",
|
||||
risk="medium",
|
||||
source="skills-index",
|
||||
source="skills/beta",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -392,7 +392,7 @@ async def test_sync_pack_clones_and_upserts_skills(monkeypatch: pytest.MonkeyPat
|
||||
by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/beta"
|
||||
].source
|
||||
== "skills-index"
|
||||
== "skills/beta"
|
||||
)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
@@ -450,7 +450,10 @@ async def test_create_skill_pack_rejects_non_https_source_url() -> None:
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "scheme" in response.json()["detail"].lower() or "https" in response.json()["detail"].lower()
|
||||
assert (
|
||||
"scheme" in response.json()["detail"].lower()
|
||||
or "https" in response.json()["detail"].lower()
|
||||
)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
@@ -480,7 +483,10 @@ async def test_create_skill_pack_rejects_localhost_source_url() -> None:
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "hostname" in response.json()["detail"].lower() or "not allowed" in response.json()["detail"].lower()
|
||||
assert (
|
||||
"hostname" in response.json()["detail"].lower()
|
||||
or "not allowed" in response.json()["detail"].lower()
|
||||
)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
@@ -724,7 +730,6 @@ def test_collect_pack_skills_from_repo_uses_root_index_when_present(tmp_path: Pa
|
||||
"path": "skills/index-first",
|
||||
"category": "uncategorized",
|
||||
"risk": "unknown",
|
||||
"source": "index-source",
|
||||
},
|
||||
{
|
||||
"id": "second",
|
||||
@@ -733,7 +738,6 @@ def test_collect_pack_skills_from_repo_uses_root_index_when_present(tmp_path: Pa
|
||||
"path": "skills/index-second/SKILL.md",
|
||||
"category": "catalog",
|
||||
"risk": "low",
|
||||
"source": "index-source",
|
||||
},
|
||||
{
|
||||
"id": "root",
|
||||
@@ -742,7 +746,6 @@ def test_collect_pack_skills_from_repo_uses_root_index_when_present(tmp_path: Pa
|
||||
"path": "SKILL.md",
|
||||
"category": "uncategorized",
|
||||
"risk": "unknown",
|
||||
"source": "index-source",
|
||||
},
|
||||
]
|
||||
),
|
||||
@@ -766,19 +769,34 @@ def test_collect_pack_skills_from_repo_uses_root_index_when_present(tmp_path: Pa
|
||||
in by_source
|
||||
)
|
||||
assert "https://github.com/sickn33/antigravity-awesome-skills/tree/main" in by_source
|
||||
assert by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].name == "Index First"
|
||||
assert by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].category == "uncategorized"
|
||||
assert by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].risk == "unknown"
|
||||
assert by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].source == "index-source"
|
||||
assert by_source["https://github.com/sickn33/antigravity-awesome-skills/tree/main"].name == "Root Skill"
|
||||
assert (
|
||||
by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].name
|
||||
== "Index First"
|
||||
)
|
||||
assert (
|
||||
by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].category
|
||||
== "uncategorized"
|
||||
)
|
||||
assert (
|
||||
by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].risk
|
||||
== "unknown"
|
||||
)
|
||||
assert (
|
||||
by_source[
|
||||
"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/index-first"
|
||||
].source
|
||||
== "skills/index-first"
|
||||
)
|
||||
assert (
|
||||
by_source["https://github.com/sickn33/antigravity-awesome-skills/tree/main"].name
|
||||
== "Root Skill"
|
||||
)
|
||||
|
||||
|
||||
def test_collect_pack_skills_from_repo_supports_root_skill_md(tmp_path: Path) -> None:
|
||||
@@ -827,8 +845,7 @@ def test_collect_pack_skills_from_repo_supports_top_level_skill_folders(
|
||||
in by_source
|
||||
)
|
||||
assert (
|
||||
"https://github.com/BrianRWagner/ai-marketing-skills/tree/main/homepage-audit"
|
||||
in by_source
|
||||
"https://github.com/BrianRWagner/ai-marketing-skills/tree/main/homepage-audit" in by_source
|
||||
)
|
||||
|
||||
|
||||
@@ -862,7 +879,6 @@ def test_collect_pack_skills_from_repo_streams_large_index(tmp_path: Path) -> No
|
||||
|
||||
assert len(skills) == 1
|
||||
assert (
|
||||
skills[0].source_url
|
||||
== "https://github.com/example/oversized-pack/tree/main/skills/ignored"
|
||||
skills[0].source_url == "https://github.com/example/oversized-pack/tree/main/skills/ignored"
|
||||
)
|
||||
assert skills[0].name == "Huge Index Skill"
|
||||
|
||||
@@ -11,7 +11,7 @@ from sqlmodel import SQLModel, col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.api.deps import ActorContext
|
||||
from app.api.tasks import _TaskUpdateInput, _apply_lead_task_update
|
||||
from app.api.tasks import _apply_lead_task_update, _TaskUpdateInput
|
||||
from app.models.agents import Agent
|
||||
from app.models.boards import Board
|
||||
from app.models.organizations import Organization
|
||||
|
||||
@@ -50,16 +50,20 @@ export default function SkillsMarketplacePage() {
|
||||
const searchParams = useSearchParams();
|
||||
const { isSignedIn } = useAuth();
|
||||
const { isAdmin } = useOrganizationMembership(isSignedIn);
|
||||
const [selectedSkill, setSelectedSkill] = useState<MarketplaceSkillCardRead | null>(null);
|
||||
const [selectedSkill, setSelectedSkill] =
|
||||
useState<MarketplaceSkillCardRead | null>(null);
|
||||
const [gatewayInstalledById, setGatewayInstalledById] = useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
const [installedGatewayNamesBySkillId, setInstalledGatewayNamesBySkillId] = useState<
|
||||
Record<string, string[]>
|
||||
>({});
|
||||
const [installedGatewayNamesBySkillId, setInstalledGatewayNamesBySkillId] =
|
||||
useState<Record<string, { id: string; name: string }[]>>({});
|
||||
const [isGatewayStatusLoading, setIsGatewayStatusLoading] = useState(false);
|
||||
const [gatewayStatusError, setGatewayStatusError] = useState<string | null>(null);
|
||||
const [installingGatewayId, setInstallingGatewayId] = useState<string | null>(null);
|
||||
const [gatewayStatusError, setGatewayStatusError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [installingGatewayId, setInstallingGatewayId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const { sorting, onSortingChange } = useUrlSorting({
|
||||
allowedColumnIds: MARKETPLACE_SKILLS_SORTABLE_COLUMNS,
|
||||
@@ -161,25 +165,29 @@ export default function SkillsMarketplacePage() {
|
||||
const updateInstalledGatewayNames = useCallback(
|
||||
({
|
||||
skillId,
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
installed,
|
||||
}: {
|
||||
skillId: string;
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
installed: boolean;
|
||||
}) => {
|
||||
setInstalledGatewayNamesBySkillId((previous) => {
|
||||
const installedOn = previous[skillId] ?? [];
|
||||
if (installed) {
|
||||
if (installedOn.includes(gatewayName)) return previous;
|
||||
if (installedOn.some((gateway) => gateway.id === gatewayId)) {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[skillId]: [...installedOn, gatewayName],
|
||||
[skillId]: [...installedOn, { id: gatewayId, name: gatewayName }],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[skillId]: installedOn.filter((name) => name !== gatewayName),
|
||||
[skillId]: installedOn.filter((gateway) => gateway.id !== gatewayId),
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -190,7 +198,12 @@ export default function SkillsMarketplacePage() {
|
||||
let cancelled = false;
|
||||
|
||||
const loadInstalledGatewaysBySkill = async () => {
|
||||
if (!isSignedIn || !isAdmin || gateways.length === 0 || skills.length === 0) {
|
||||
if (
|
||||
!isSignedIn ||
|
||||
!isAdmin ||
|
||||
gateways.length === 0 ||
|
||||
skills.length === 0
|
||||
) {
|
||||
setInstalledGatewayNamesBySkillId({});
|
||||
return;
|
||||
}
|
||||
@@ -198,9 +211,10 @@ export default function SkillsMarketplacePage() {
|
||||
try {
|
||||
const gatewaySkills = await Promise.all(
|
||||
gateways.map(async (gateway) => {
|
||||
const response = await listMarketplaceSkillsApiV1SkillsMarketplaceGet({
|
||||
gateway_id: gateway.id,
|
||||
});
|
||||
const response =
|
||||
await listMarketplaceSkillsApiV1SkillsMarketplaceGet({
|
||||
gateway_id: gateway.id,
|
||||
});
|
||||
return {
|
||||
gatewayId: gateway.id,
|
||||
gatewayName: gateway.name,
|
||||
@@ -211,16 +225,26 @@ export default function SkillsMarketplacePage() {
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const nextInstalledGatewayNamesBySkillId: Record<string, string[]> = {};
|
||||
const nextInstalledGatewayNamesBySkillId: Record<
|
||||
string,
|
||||
{ id: string; name: string }[]
|
||||
> = {};
|
||||
for (const skill of skills) {
|
||||
nextInstalledGatewayNamesBySkillId[skill.id] = [];
|
||||
}
|
||||
|
||||
for (const { gatewayName, skills: gatewaySkillRows } of gatewaySkills) {
|
||||
for (const {
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
skills: gatewaySkillRows,
|
||||
} of gatewaySkills) {
|
||||
for (const skill of gatewaySkillRows) {
|
||||
if (!skill.installed) continue;
|
||||
if (!nextInstalledGatewayNamesBySkillId[skill.id]) continue;
|
||||
nextInstalledGatewayNamesBySkillId[skill.id].push(gatewayName);
|
||||
nextInstalledGatewayNamesBySkillId[skill.id].push({
|
||||
id: gatewayId,
|
||||
name: gatewayName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,11 +274,13 @@ export default function SkillsMarketplacePage() {
|
||||
...previous,
|
||||
[variables.params.gateway_id]: true,
|
||||
}));
|
||||
const gatewayName =
|
||||
gateways.find((gateway) => gateway.id === variables.params.gateway_id)?.name;
|
||||
const gatewayName = gateways.find(
|
||||
(gateway) => gateway.id === variables.params.gateway_id,
|
||||
)?.name;
|
||||
if (gatewayName) {
|
||||
updateInstalledGatewayNames({
|
||||
skillId: variables.skillId,
|
||||
gatewayId: variables.params.gateway_id,
|
||||
gatewayName,
|
||||
installed: true,
|
||||
});
|
||||
@@ -277,11 +303,13 @@ export default function SkillsMarketplacePage() {
|
||||
...previous,
|
||||
[variables.params.gateway_id]: false,
|
||||
}));
|
||||
const gatewayName =
|
||||
gateways.find((gateway) => gateway.id === variables.params.gateway_id)?.name;
|
||||
const gatewayName = gateways.find(
|
||||
(gateway) => gateway.id === variables.params.gateway_id,
|
||||
)?.name;
|
||||
if (gatewayName) {
|
||||
updateInstalledGatewayNames({
|
||||
skillId: variables.skillId,
|
||||
gatewayId: variables.params.gateway_id,
|
||||
gatewayName,
|
||||
installed: false,
|
||||
});
|
||||
@@ -314,16 +342,22 @@ export default function SkillsMarketplacePage() {
|
||||
setGatewayStatusError(null);
|
||||
try {
|
||||
const gatewaySkills = await loadSkillsByGateway();
|
||||
const entries = gatewaySkills.map(({ gatewayId, skills: gatewaySkillRows }) => {
|
||||
const row = gatewaySkillRows.find((skill) => skill.id === selectedSkill.id);
|
||||
return [gatewayId, Boolean(row?.installed)] as const;
|
||||
});
|
||||
const entries = gatewaySkills.map(
|
||||
({ gatewayId, skills: gatewaySkillRows }) => {
|
||||
const row = gatewaySkillRows.find(
|
||||
(skill) => skill.id === selectedSkill.id,
|
||||
);
|
||||
return [gatewayId, Boolean(row?.installed)] as const;
|
||||
},
|
||||
);
|
||||
if (cancelled) return;
|
||||
setGatewayInstalledById(Object.fromEntries(entries));
|
||||
} catch (error) {
|
||||
if (cancelled) return;
|
||||
setGatewayStatusError(
|
||||
error instanceof Error ? error.message : "Unable to load gateway status.",
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unable to load gateway status.",
|
||||
);
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
@@ -391,7 +425,9 @@ export default function SkillsMarketplacePage() {
|
||||
<div className="space-y-6">
|
||||
{gateways.length === 0 ? (
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-6 text-sm text-slate-600 shadow-sm">
|
||||
<p className="font-medium text-slate-900">No gateways available yet.</p>
|
||||
<p className="font-medium text-slate-900">
|
||||
No gateways available yet.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Create a gateway first, then return here to manage installs.
|
||||
</p>
|
||||
@@ -407,7 +443,9 @@ export default function SkillsMarketplacePage() {
|
||||
<div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<MarketplaceSkillsTable
|
||||
skills={visibleSkills}
|
||||
installedGatewayNamesBySkillId={installedGatewayNamesBySkillId}
|
||||
installedGatewayNamesBySkillId={
|
||||
installedGatewayNamesBySkillId
|
||||
}
|
||||
isLoading={skillsQuery.isLoading}
|
||||
sorting={sorting}
|
||||
onSortingChange={onSortingChange}
|
||||
@@ -416,7 +454,8 @@ export default function SkillsMarketplacePage() {
|
||||
onSkillClick={setSelectedSkill}
|
||||
emptyState={{
|
||||
title: "No marketplace skills yet",
|
||||
description: "Add packs first, then synced skills will appear here.",
|
||||
description:
|
||||
"Add packs first, then synced skills will appear here.",
|
||||
actionHref: "/skills/packs/new",
|
||||
actionLabel: "Add your first pack",
|
||||
}}
|
||||
@@ -431,7 +470,9 @@ export default function SkillsMarketplacePage() {
|
||||
{packsQuery.error ? (
|
||||
<p className="text-sm text-rose-600">{packsQuery.error.message}</p>
|
||||
) : null}
|
||||
{mutationError ? <p className="text-sm text-rose-600">{mutationError}</p> : null}
|
||||
{mutationError ? (
|
||||
<p className="text-sm text-rose-600">{mutationError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</DashboardPageLayout>
|
||||
|
||||
|
||||
@@ -36,11 +36,10 @@ export default function EditSkillPackPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const pack = (
|
||||
packQuery.data?.status === 200 ? packQuery.data.data : null
|
||||
);
|
||||
const pack = packQuery.data?.status === 200 ? packQuery.data.data : null;
|
||||
|
||||
const saveMutation = useUpdateSkillPackApiV1SkillsPacksPackIdPatch<ApiError>();
|
||||
const saveMutation =
|
||||
useUpdateSkillPackApiV1SkillsPacksPackIdPatch<ApiError>();
|
||||
|
||||
return (
|
||||
<DashboardPageLayout
|
||||
|
||||
@@ -80,19 +80,18 @@ export default function SkillsPacksPage() {
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
const syncMutation =
|
||||
useSyncSkillPackApiV1SkillsPacksPackIdSyncPost<ApiError>(
|
||||
{
|
||||
mutation: {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: packsQueryKey,
|
||||
});
|
||||
},
|
||||
const syncMutation = useSyncSkillPackApiV1SkillsPacksPackIdSyncPost<ApiError>(
|
||||
{
|
||||
mutation: {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: packsQueryKey,
|
||||
});
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!deleteTarget) return;
|
||||
@@ -113,7 +112,9 @@ export default function SkillsPacksPage() {
|
||||
const response = await syncMutation.mutateAsync({
|
||||
packId: pack.id,
|
||||
});
|
||||
setSyncWarnings(response.data.warnings ?? []);
|
||||
if (response.status === 200) {
|
||||
setSyncWarnings(response.data.warnings ?? []);
|
||||
}
|
||||
} finally {
|
||||
setSyncingPackIds((previous) => {
|
||||
const next = new Set(previous);
|
||||
@@ -124,7 +125,12 @@ export default function SkillsPacksPage() {
|
||||
};
|
||||
|
||||
const handleSyncAllPacks = async () => {
|
||||
if (!isAdmin || isSyncingAll || syncingPackIds.size > 0 || packs.length === 0) {
|
||||
if (
|
||||
!isAdmin ||
|
||||
isSyncingAll ||
|
||||
syncingPackIds.size > 0 ||
|
||||
packs.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,10 +151,12 @@ export default function SkillsPacksPage() {
|
||||
|
||||
try {
|
||||
const response = await syncMutation.mutateAsync({ packId: pack.id });
|
||||
setSyncWarnings((previous) => [
|
||||
...previous,
|
||||
...(response.data.warnings ?? []),
|
||||
]);
|
||||
if (response.status === 200) {
|
||||
setSyncWarnings((previous) => [
|
||||
...previous,
|
||||
...(response.data.warnings ?? []),
|
||||
]);
|
||||
}
|
||||
} catch {
|
||||
hasFailure = true;
|
||||
} finally {
|
||||
@@ -190,9 +198,7 @@ export default function SkillsPacksPage() {
|
||||
size: "md",
|
||||
})}
|
||||
disabled={
|
||||
isSyncingAll ||
|
||||
syncingPackIds.size > 0 ||
|
||||
packs.length === 0
|
||||
isSyncingAll || syncingPackIds.size > 0 || packs.length === 0
|
||||
}
|
||||
onClick={() => {
|
||||
void handleSyncAllPacks();
|
||||
@@ -241,10 +247,14 @@ export default function SkillsPacksPage() {
|
||||
<p className="text-sm text-rose-600">{packsQuery.error.message}</p>
|
||||
) : null}
|
||||
{deleteMutation.error ? (
|
||||
<p className="text-sm text-rose-600">{deleteMutation.error.message}</p>
|
||||
<p className="text-sm text-rose-600">
|
||||
{deleteMutation.error.message}
|
||||
</p>
|
||||
) : null}
|
||||
{syncMutation.error ? (
|
||||
<p className="text-sm text-rose-600">{syncMutation.error.message}</p>
|
||||
<p className="text-sm text-rose-600">
|
||||
{syncMutation.error.message}
|
||||
</p>
|
||||
) : null}
|
||||
{syncAllError ? (
|
||||
<p className="text-sm text-rose-600">{syncAllError}</p>
|
||||
|
||||
@@ -177,7 +177,8 @@ export function DashboardSidebar() {
|
||||
href="/skills/marketplace"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2.5 text-slate-700 transition",
|
||||
pathname === "/skills" || pathname.startsWith("/skills/marketplace")
|
||||
pathname === "/skills" ||
|
||||
pathname.startsWith("/skills/marketplace")
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "hover:bg-slate-100",
|
||||
)}
|
||||
|
||||
@@ -11,9 +11,13 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import type { MarketplaceSkillCardRead } from "@/api/generated/model";
|
||||
import { DataTable, type DataTableEmptyState } from "@/components/tables/DataTable";
|
||||
import {
|
||||
DataTable,
|
||||
type DataTableEmptyState,
|
||||
} from "@/components/tables/DataTable";
|
||||
import { dateCell } from "@/components/tables/cell-formatters";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
SKILLS_TABLE_EMPTY_ICON,
|
||||
useTableSortingState,
|
||||
@@ -25,9 +29,32 @@ import {
|
||||
packsHrefFromPackUrl,
|
||||
} from "@/lib/skills-source";
|
||||
|
||||
function riskBadgeVariant(risk: string | null | undefined) {
|
||||
const normalizedRisk = (risk || "unknown").trim().toLowerCase();
|
||||
|
||||
switch (normalizedRisk) {
|
||||
case "low":
|
||||
return "success";
|
||||
case "medium":
|
||||
case "moderate":
|
||||
return "warning";
|
||||
case "high":
|
||||
case "critical":
|
||||
return "danger";
|
||||
case "unknown":
|
||||
return "outline";
|
||||
default:
|
||||
return "accent";
|
||||
}
|
||||
}
|
||||
|
||||
function riskBadgeLabel(risk: string | null | undefined) {
|
||||
return (risk || "unknown").trim() || "unknown";
|
||||
}
|
||||
|
||||
type MarketplaceSkillsTableProps = {
|
||||
skills: MarketplaceSkillCardRead[];
|
||||
installedGatewayNamesBySkillId?: Record<string, string[]>;
|
||||
installedGatewayNamesBySkillId?: Record<string, { id: string; name: string }[]>;
|
||||
isLoading?: boolean;
|
||||
sorting?: SortingState;
|
||||
onSortingChange?: OnChangeFn<SortingState>;
|
||||
@@ -78,7 +105,9 @@ export function MarketplaceSkillsTable({
|
||||
{row.original.name}
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-sm font-medium text-slate-900">{row.original.name}</p>
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{row.original.name}
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
className="mt-1 line-clamp-2 text-xs text-slate-500"
|
||||
@@ -117,9 +146,12 @@ export function MarketplaceSkillsTable({
|
||||
accessorKey: "risk",
|
||||
header: "Risk",
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-slate-700">
|
||||
{row.original.risk || "unknown"}
|
||||
</span>
|
||||
<Badge
|
||||
variant={riskBadgeVariant(row.original.risk)}
|
||||
className="px-2 py-0.5"
|
||||
>
|
||||
{riskBadgeLabel(row.original.risk)}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -127,6 +159,11 @@ export function MarketplaceSkillsTable({
|
||||
header: "Source",
|
||||
cell: ({ row }) => {
|
||||
const sourceHref = row.original.source || row.original.source_url;
|
||||
|
||||
if (!sourceHref) {
|
||||
return <span className="text-sm text-slate-400">No source</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={sourceHref}
|
||||
@@ -145,15 +182,32 @@ export function MarketplaceSkillsTable({
|
||||
header: "Installed On",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const installedOn = installedGatewayNamesBySkillId?.[row.original.id] ?? [];
|
||||
const installedOn =
|
||||
installedGatewayNamesBySkillId?.[row.original.id] ?? [];
|
||||
if (installedOn.length === 0) {
|
||||
return <span className="text-sm text-slate-500">-</span>;
|
||||
}
|
||||
const installedOnText = installedOn.join(", ");
|
||||
return (
|
||||
<span className="text-sm text-slate-700" title={installedOnText}>
|
||||
{installedOnText}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{installedOn.map((gateway, index) => {
|
||||
const isLast = index === installedOn.length - 1;
|
||||
return (
|
||||
<span
|
||||
key={`${gateway.id}-${index}`}
|
||||
className="inline-flex items-center gap-1 text-sm text-slate-700"
|
||||
title={gateway.name}
|
||||
>
|
||||
<Link
|
||||
href={`/gateways/${gateway.id}`}
|
||||
className="text-blue-700 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
{gateway.name}
|
||||
</Link>
|
||||
{!isLast ? "," : ""}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -48,7 +48,9 @@ export function SkillInstallDialog({
|
||||
className="max-w-xl p-6 sm:p-7"
|
||||
>
|
||||
<DialogHeader className="pb-1">
|
||||
<DialogTitle>{selectedSkill ? selectedSkill.name : "Install skill"}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{selectedSkill ? selectedSkill.name : "Install skill"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose one or more gateways where this skill should be installed.
|
||||
</DialogDescription>
|
||||
@@ -60,14 +62,17 @@ export function SkillInstallDialog({
|
||||
) : (
|
||||
gateways.map((gateway) => {
|
||||
const isInstalled = gatewayInstalledById[gateway.id] === true;
|
||||
const isUpdatingGateway = installingGatewayId === gateway.id && isMutating;
|
||||
const isUpdatingGateway =
|
||||
installingGatewayId === gateway.id && isMutating;
|
||||
return (
|
||||
<div
|
||||
key={gateway.id}
|
||||
className="flex items-center justify-between rounded-xl border border-slate-200 bg-white p-4"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900">{gateway.name}</p>
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{gateway.name}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -91,11 +96,17 @@ export function SkillInstallDialog({
|
||||
{gatewayStatusError ? (
|
||||
<p className="text-sm text-rose-600">{gatewayStatusError}</p>
|
||||
) : null}
|
||||
{mutationError ? <p className="text-sm text-rose-600">{mutationError}</p> : null}
|
||||
{mutationError ? (
|
||||
<p className="text-sm text-rose-600">{mutationError}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-6 border-t border-slate-200 pt-4">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isMutating}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={isMutating}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import type { SkillPackRead } from "@/api/generated/model";
|
||||
import { DataTable, type DataTableEmptyState } from "@/components/tables/DataTable";
|
||||
import {
|
||||
DataTable,
|
||||
type DataTableEmptyState,
|
||||
} from "@/components/tables/DataTable";
|
||||
import { dateCell } from "@/components/tables/cell-formatters";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -62,7 +65,9 @@ export function SkillPacksTable({
|
||||
header: "Pack",
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900">{row.original.name}</p>
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{row.original.name}
|
||||
</p>
|
||||
<p className="mt-1 line-clamp-2 text-xs text-slate-500">
|
||||
{row.original.description || "No description provided."}
|
||||
</p>
|
||||
@@ -86,7 +91,11 @@ export function SkillPacksTable({
|
||||
{
|
||||
accessorKey: "branch",
|
||||
header: "Branch",
|
||||
cell: ({ row }) => <p className="text-sm text-slate-900">{row.original.branch || "main"}</p>,
|
||||
cell: ({ row }) => (
|
||||
<p className="text-sm text-slate-900">
|
||||
{row.original.branch || "main"}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "skill_count",
|
||||
@@ -111,7 +120,9 @@ export function SkillPacksTable({
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
if (!onSync) return null;
|
||||
const isThisPackSyncing = Boolean(syncingPackIds?.has(row.original.id));
|
||||
const isThisPackSyncing = Boolean(
|
||||
syncingPackIds?.has(row.original.id),
|
||||
);
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
|
||||
@@ -34,7 +34,8 @@ export const useTableSortingState = (
|
||||
resolvedSorting: SortingState;
|
||||
handleSortingChange: OnChangeFn<SortingState>;
|
||||
} => {
|
||||
const [internalSorting, setInternalSorting] = useState<SortingState>(defaultSorting);
|
||||
const [internalSorting, setInternalSorting] =
|
||||
useState<SortingState>(defaultSorting);
|
||||
const resolvedSorting = sorting ?? internalSorting;
|
||||
const handleSortingChange: OnChangeFn<SortingState> =
|
||||
onSortingChange ??
|
||||
|
||||
@@ -3,7 +3,9 @@ export const normalizeRepoSourceUrl = (sourceUrl: string): string => {
|
||||
return trimmed.endsWith(".git") ? trimmed.slice(0, -4) : trimmed;
|
||||
};
|
||||
|
||||
export const repoBaseFromSkillSourceUrl = (skillSourceUrl: string): string | null => {
|
||||
export const repoBaseFromSkillSourceUrl = (
|
||||
skillSourceUrl: string,
|
||||
): string | null => {
|
||||
try {
|
||||
const parsed = new URL(skillSourceUrl);
|
||||
const marker = "/tree/";
|
||||
@@ -11,7 +13,8 @@ export const repoBaseFromSkillSourceUrl = (skillSourceUrl: string): string | nul
|
||||
if (markerIndex <= 0) return null;
|
||||
|
||||
// Reject unexpected structures (e.g. multiple /tree/ markers).
|
||||
if (parsed.pathname.indexOf(marker, markerIndex + marker.length) !== -1) return null;
|
||||
if (parsed.pathname.indexOf(marker, markerIndex + marker.length) !== -1)
|
||||
return null;
|
||||
|
||||
const repoPath = parsed.pathname.slice(0, markerIndex);
|
||||
if (!repoPath || repoPath === "/") return null;
|
||||
|
||||
Reference in New Issue
Block a user