feat: Refactor tool management and update documentation for clarity
This commit is contained in:
@@ -18,7 +18,6 @@ from castle_cli.manifest import (
|
|||||||
RunPythonUvTool,
|
RunPythonUvTool,
|
||||||
SystemdSpec,
|
SystemdSpec,
|
||||||
ToolSpec,
|
ToolSpec,
|
||||||
ToolType,
|
|
||||||
)
|
)
|
||||||
from castle_cli.templates.scaffold import scaffold_category_tool, scaffold_project
|
from castle_cli.templates.scaffold import scaffold_category_tool, scaffold_project
|
||||||
|
|
||||||
@@ -102,10 +101,7 @@ def run_create(args: argparse.Namespace) -> int:
|
|||||||
manifest = ComponentManifest(
|
manifest = ComponentManifest(
|
||||||
id=name,
|
id=name,
|
||||||
description=args.description or f"A castle {proj_type}",
|
description=args.description or f"A castle {proj_type}",
|
||||||
tool=ToolSpec(
|
tool=ToolSpec(source=f"{name}/"),
|
||||||
tool_type=ToolType.PYTHON_STANDALONE,
|
|
||||||
source=f"{name}/",
|
|
||||||
),
|
|
||||||
install=InstallSpec(path=PathInstallSpec(alias=name)),
|
install=InstallSpec(path=PathInstallSpec(alias=name)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -173,11 +169,7 @@ def _create_category_tool(config: object, name: str, description: str | None, ca
|
|||||||
manifest = ComponentManifest(
|
manifest = ComponentManifest(
|
||||||
id=name,
|
id=name,
|
||||||
description=desc,
|
description=desc,
|
||||||
tool=ToolSpec(
|
tool=ToolSpec(source=f"tools/{category}/"),
|
||||||
tool_type=ToolType.PYTHON_STANDALONE,
|
|
||||||
category=category,
|
|
||||||
source=f"tools/{category}/",
|
|
||||||
),
|
|
||||||
install=InstallSpec(path=PathInstallSpec(alias=name)),
|
install=InstallSpec(path=PathInstallSpec(alias=name)),
|
||||||
)
|
)
|
||||||
config.components[name] = manifest
|
config.components[name] = manifest
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ def run_info(args: argparse.Namespace) -> int:
|
|||||||
# Tool
|
# Tool
|
||||||
if manifest.tool:
|
if manifest.tool:
|
||||||
t = manifest.tool
|
t = manifest.tool
|
||||||
print(f" {BOLD}category{RESET}: {t.category or 'uncategorized'}")
|
|
||||||
if t.source:
|
if t.source:
|
||||||
print(f" {BOLD}source{RESET}: {t.source}")
|
print(f" {BOLD}source{RESET}: {t.source}")
|
||||||
if t.system_dependencies:
|
if t.system_dependencies:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from castle_cli.config import ensure_dirs, load_config
|
from castle_cli.config import ensure_dirs, load_config
|
||||||
from castle_cli.manifest import ToolType
|
|
||||||
|
|
||||||
|
|
||||||
def run_sync(args: argparse.Namespace) -> int:
|
def run_sync(args: argparse.Namespace) -> int:
|
||||||
@@ -46,7 +45,7 @@ def run_sync(args: argparse.Namespace) -> int:
|
|||||||
print(" OK")
|
print(" OK")
|
||||||
synced_dirs.add(project_dir)
|
synced_dirs.add(project_dir)
|
||||||
|
|
||||||
# Install tools by type
|
# Install tools — infer method from project structure
|
||||||
uv_path = shutil.which("uv") or "uv"
|
uv_path = shutil.which("uv") or "uv"
|
||||||
installed_dirs: set[Path] = set()
|
installed_dirs: set[Path] = set()
|
||||||
|
|
||||||
@@ -54,18 +53,19 @@ def run_sync(args: argparse.Namespace) -> int:
|
|||||||
if not manifest.tool:
|
if not manifest.tool:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if manifest.tool.tool_type == ToolType.PYTHON_STANDALONE:
|
|
||||||
source = manifest.tool.source
|
source = manifest.tool.source
|
||||||
if not source:
|
if not source:
|
||||||
continue
|
continue
|
||||||
project_dir = config.root / source
|
|
||||||
if not (project_dir / "pyproject.toml").exists():
|
source_dir = config.root / source
|
||||||
continue
|
|
||||||
if project_dir in installed_dirs:
|
if (source_dir / "pyproject.toml").exists():
|
||||||
|
# Python package — uv tool install
|
||||||
|
if source_dir in installed_dirs:
|
||||||
continue
|
continue
|
||||||
print(f"\nInstalling {name}...")
|
print(f"\nInstalling {name}...")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[uv_path, "tool", "install", "--editable", str(project_dir),
|
[uv_path, "tool", "install", "--editable", str(source_dir),
|
||||||
"--force"],
|
"--force"],
|
||||||
capture_output=True, text=True,
|
capture_output=True, text=True,
|
||||||
)
|
)
|
||||||
@@ -77,23 +77,19 @@ def run_sync(args: argparse.Namespace) -> int:
|
|||||||
all_ok = False
|
all_ok = False
|
||||||
else:
|
else:
|
||||||
print(f" {name}: OK")
|
print(f" {name}: OK")
|
||||||
installed_dirs.add(project_dir)
|
installed_dirs.add(source_dir)
|
||||||
|
|
||||||
elif manifest.tool.tool_type == ToolType.SCRIPT:
|
elif source_dir.is_file():
|
||||||
# Verify script tools are accessible
|
# Script file — symlink to ~/.local/bin/
|
||||||
alias = name
|
alias = name
|
||||||
if manifest.install and manifest.install.path and manifest.install.path.alias:
|
if manifest.install and manifest.install.path and manifest.install.path.alias:
|
||||||
alias = manifest.install.path.alias
|
alias = manifest.install.path.alias
|
||||||
if not shutil.which(alias):
|
if not shutil.which(alias):
|
||||||
source = manifest.tool.source
|
|
||||||
if source:
|
|
||||||
script_path = config.root / source
|
|
||||||
if script_path.exists():
|
|
||||||
link = Path.home() / ".local" / "bin" / alias
|
link = Path.home() / ".local" / "bin" / alias
|
||||||
link.parent.mkdir(parents=True, exist_ok=True)
|
link.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if not link.exists():
|
if not link.exists():
|
||||||
link.symlink_to(script_path)
|
link.symlink_to(source_dir)
|
||||||
print(f"\n Linked {alias} → {script_path}")
|
print(f"\n Linked {alias} → {source_dir}")
|
||||||
|
|
||||||
if all_ok:
|
if all_ok:
|
||||||
print("\nAll projects synced.")
|
print("\nAll projects synced.")
|
||||||
|
|||||||
@@ -36,14 +36,17 @@ def _tool_list() -> int:
|
|||||||
print("No tools registered.")
|
print("No tools registered.")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
by_category: dict[str, list[tuple[str, object]]] = {}
|
by_group: dict[str, list[tuple[str, object]]] = {}
|
||||||
for name, manifest in tools.items():
|
for name, manifest in tools.items():
|
||||||
cat = manifest.tool.category or "uncategorized"
|
if manifest.tool and manifest.tool.source:
|
||||||
by_category.setdefault(cat, []).append((name, manifest))
|
group = Path(manifest.tool.source).name
|
||||||
|
else:
|
||||||
|
group = "standalone"
|
||||||
|
by_group.setdefault(group, []).append((name, manifest))
|
||||||
|
|
||||||
for category in sorted(by_category):
|
for group in sorted(by_group):
|
||||||
items = by_category[category]
|
items = by_group[group]
|
||||||
print(f"\n{BOLD}{CYAN}{category}{RESET}")
|
print(f"\n{BOLD}{CYAN}{group}{RESET}")
|
||||||
print(f"{CYAN}{'─' * 40}{RESET}")
|
print(f"{CYAN}{'─' * 40}{RESET}")
|
||||||
for name, manifest in sorted(items):
|
for name, manifest in sorted(items):
|
||||||
desc = manifest.description or ""
|
desc = manifest.description or ""
|
||||||
@@ -74,7 +77,6 @@ def _tool_info(name: str) -> int:
|
|||||||
print(f"{'─' * 40}")
|
print(f"{'─' * 40}")
|
||||||
if manifest.description:
|
if manifest.description:
|
||||||
print(f" {manifest.description}")
|
print(f" {manifest.description}")
|
||||||
print(f" {BOLD}category{RESET}: {t.category or 'uncategorized'}")
|
|
||||||
print(f" {BOLD}version{RESET}: {t.version}")
|
print(f" {BOLD}version{RESET}: {t.version}")
|
||||||
if t.source:
|
if t.source:
|
||||||
print(f" {BOLD}source{RESET}: {t.source}")
|
print(f" {BOLD}source{RESET}: {t.source}")
|
||||||
@@ -83,7 +85,7 @@ def _tool_info(name: str) -> int:
|
|||||||
|
|
||||||
# Read and display .md documentation if available
|
# Read and display .md documentation if available
|
||||||
if t.source:
|
if t.source:
|
||||||
md_path = _find_md_for_tool(config.root, t.source, name, t.category)
|
md_path = _find_md_for_tool(config.root, t.source, name)
|
||||||
if md_path and md_path.exists():
|
if md_path and md_path.exists():
|
||||||
content = md_path.read_text()
|
content = md_path.read_text()
|
||||||
# Strip YAML frontmatter
|
# Strip YAML frontmatter
|
||||||
@@ -102,7 +104,7 @@ def _tool_info(name: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _find_md_for_tool(
|
def _find_md_for_tool(
|
||||||
root: Path, source: str, tool_name: str, category: str | None = None,
|
root: Path, source: str, tool_name: str,
|
||||||
) -> Path | None:
|
) -> Path | None:
|
||||||
"""Find the .md documentation file for a tool source path."""
|
"""Find the .md documentation file for a tool source path."""
|
||||||
source_path = root / source
|
source_path = root / source
|
||||||
@@ -111,10 +113,9 @@ def _find_md_for_tool(
|
|||||||
if md.exists():
|
if md.exists():
|
||||||
return md
|
return md
|
||||||
elif source_path.is_dir():
|
elif source_path.is_dir():
|
||||||
# Directory source — look for src/<category>/<tool_name>.md
|
|
||||||
py_name = tool_name.replace("-", "_")
|
py_name = tool_name.replace("-", "_")
|
||||||
if category:
|
pkg_name = source_path.name
|
||||||
md = source_path / "src" / category / f"{py_name}.md"
|
md = source_path / "src" / pkg_name / f"{py_name}.md"
|
||||||
if md.exists():
|
if md.exists():
|
||||||
return md
|
return md
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Annotated, Literal, Union
|
from typing import Annotated, Literal, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, computed_field, model_validator
|
from pydantic import BaseModel, ConfigDict, Field, computed_field, model_validator
|
||||||
|
|
||||||
EnvMap = dict[str, str]
|
EnvMap = dict[str, str]
|
||||||
|
|
||||||
@@ -143,6 +143,7 @@ class SystemdSpec(BaseModel):
|
|||||||
restart_sec: int = 2
|
restart_sec: int = 2
|
||||||
no_new_privileges: bool = True
|
no_new_privileges: bool = True
|
||||||
readiness: ReadinessHttpGet | None = None
|
readiness: ReadinessHttpGet | None = None
|
||||||
|
exec_reload: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ManageSpec(BaseModel):
|
class ManageSpec(BaseModel):
|
||||||
@@ -169,14 +170,9 @@ class InstallSpec(BaseModel):
|
|||||||
# ---------------------
|
# ---------------------
|
||||||
|
|
||||||
|
|
||||||
class ToolType(str, Enum):
|
|
||||||
PYTHON_STANDALONE = "python_standalone"
|
|
||||||
SCRIPT = "script"
|
|
||||||
|
|
||||||
|
|
||||||
class ToolSpec(BaseModel):
|
class ToolSpec(BaseModel):
|
||||||
tool_type: ToolType = ToolType.PYTHON_STANDALONE
|
model_config = ConfigDict(extra="ignore")
|
||||||
category: str | None = None
|
|
||||||
version: str = "1.0.0"
|
version: str = "1.0.0"
|
||||||
source: str | None = None
|
source: str | None = None
|
||||||
system_dependencies: list[str] = Field(default_factory=list)
|
system_dependencies: list[str] = Field(default_factory=list)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from castle_cli.manifest import (
|
|||||||
RunRemote,
|
RunRemote,
|
||||||
SystemdSpec,
|
SystemdSpec,
|
||||||
ToolSpec,
|
ToolSpec,
|
||||||
ToolType,
|
|
||||||
TriggerSchedule,
|
TriggerSchedule,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,11 +95,7 @@ class TestRoleDerivation:
|
|||||||
"""Component with tool spec gets TOOL role."""
|
"""Component with tool spec gets TOOL role."""
|
||||||
m = ComponentManifest(
|
m = ComponentManifest(
|
||||||
id="docx2md",
|
id="docx2md",
|
||||||
tool=ToolSpec(
|
tool=ToolSpec(source="tools/document/"),
|
||||||
tool_type=ToolType.PYTHON_STANDALONE,
|
|
||||||
category="document",
|
|
||||||
source="tools/document/",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
assert Role.TOOL in m.roles
|
assert Role.TOOL in m.roles
|
||||||
|
|
||||||
@@ -108,7 +103,7 @@ class TestRoleDerivation:
|
|||||||
"""Tool spec alone is enough for TOOL role, no install.path needed."""
|
"""Tool spec alone is enough for TOOL role, no install.path needed."""
|
||||||
m = ComponentManifest(
|
m = ComponentManifest(
|
||||||
id="my-tool",
|
id="my-tool",
|
||||||
tool=ToolSpec(category="misc"),
|
tool=ToolSpec(),
|
||||||
)
|
)
|
||||||
assert Role.TOOL in m.roles
|
assert Role.TOOL in m.roles
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class SystemdInfo(BaseModel):
|
||||||
|
"""Systemd unit information for a managed component."""
|
||||||
|
|
||||||
|
unit_name: str
|
||||||
|
unit_path: str
|
||||||
|
timer: bool = False
|
||||||
|
|
||||||
|
|
||||||
class ComponentSummary(BaseModel):
|
class ComponentSummary(BaseModel):
|
||||||
"""Summary of a single component."""
|
"""Summary of a single component."""
|
||||||
|
|
||||||
@@ -14,9 +22,8 @@ class ComponentSummary(BaseModel):
|
|||||||
health_path: str | None = None
|
health_path: str | None = None
|
||||||
proxy_path: str | None = None
|
proxy_path: str | None = None
|
||||||
managed: bool = False
|
managed: bool = False
|
||||||
category: str | None = None
|
systemd: SystemdInfo | None = None
|
||||||
version: str | None = None
|
version: str | None = None
|
||||||
tool_type: str | None = None
|
|
||||||
source: str | None = None
|
source: str | None = None
|
||||||
system_dependencies: list[str] = []
|
system_dependencies: list[str] = []
|
||||||
schedule: str | None = None
|
schedule: str | None = None
|
||||||
@@ -65,9 +72,7 @@ class ToolSummary(BaseModel):
|
|||||||
|
|
||||||
id: str
|
id: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
category: str | None = None
|
|
||||||
source: str | None = None
|
source: str | None = None
|
||||||
tool_type: str | None = None
|
|
||||||
version: str | None = None
|
version: str | None = None
|
||||||
runner: str | None = None
|
runner: str | None = None
|
||||||
system_dependencies: list[str] = []
|
system_dependencies: list[str] = []
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
|
||||||
@@ -15,12 +16,13 @@ from dashboard_api.models import (
|
|||||||
ComponentSummary,
|
ComponentSummary,
|
||||||
GatewayInfo,
|
GatewayInfo,
|
||||||
StatusResponse,
|
StatusResponse,
|
||||||
|
SystemdInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter(tags=["dashboard"])
|
router = APIRouter(tags=["dashboard"])
|
||||||
|
|
||||||
|
|
||||||
def _summary_from_manifest(name: str, manifest: object) -> ComponentSummary:
|
def _summary_from_manifest(name: str, manifest: object, root: Path) -> ComponentSummary:
|
||||||
"""Build a ComponentSummary from a manifest."""
|
"""Build a ComponentSummary from a manifest."""
|
||||||
port = None
|
port = None
|
||||||
health_path = None
|
health_path = None
|
||||||
@@ -35,6 +37,18 @@ def _summary_from_manifest(name: str, manifest: object) -> ComponentSummary:
|
|||||||
manifest.manage and manifest.manage.systemd and manifest.manage.systemd.enable
|
manifest.manage and manifest.manage.systemd and manifest.manage.systemd.enable
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Systemd info for managed components
|
||||||
|
systemd_info: SystemdInfo | None = None
|
||||||
|
if managed:
|
||||||
|
unit_name = f"castle-{name}.service"
|
||||||
|
unit_path = str(Path("~/.config/systemd/user") / unit_name)
|
||||||
|
has_timer = any(getattr(t, "type", None) == "schedule" for t in manifest.triggers)
|
||||||
|
systemd_info = SystemdInfo(
|
||||||
|
unit_name=unit_name,
|
||||||
|
unit_path=unit_path,
|
||||||
|
timer=has_timer,
|
||||||
|
)
|
||||||
|
|
||||||
# Extract cron schedule from first schedule trigger, if any
|
# Extract cron schedule from first schedule trigger, if any
|
||||||
schedule = None
|
schedule = None
|
||||||
for t in manifest.triggers:
|
for t in manifest.triggers:
|
||||||
@@ -42,6 +56,15 @@ def _summary_from_manifest(name: str, manifest: object) -> ComponentSummary:
|
|||||||
schedule = t.cron
|
schedule = t.cron
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Infer runner — from run block or from tool source
|
||||||
|
runner = manifest.run.runner if manifest.run else None
|
||||||
|
if runner is None and manifest.tool and manifest.tool.source:
|
||||||
|
source_dir = root / manifest.tool.source
|
||||||
|
if (source_dir / "pyproject.toml").exists():
|
||||||
|
runner = "python_uv_tool"
|
||||||
|
elif source_dir.is_file():
|
||||||
|
runner = "command"
|
||||||
|
|
||||||
# Check if tool is actually installed on PATH
|
# Check if tool is actually installed on PATH
|
||||||
installed: bool | None = None
|
installed: bool | None = None
|
||||||
if manifest.install and manifest.install.path:
|
if manifest.install and manifest.install.path:
|
||||||
@@ -52,14 +75,13 @@ def _summary_from_manifest(name: str, manifest: object) -> ComponentSummary:
|
|||||||
id=name,
|
id=name,
|
||||||
description=manifest.description,
|
description=manifest.description,
|
||||||
roles=[r.value for r in manifest.roles],
|
roles=[r.value for r in manifest.roles],
|
||||||
runner=manifest.run.runner if manifest.run else None,
|
runner=runner,
|
||||||
port=port,
|
port=port,
|
||||||
health_path=health_path,
|
health_path=health_path,
|
||||||
proxy_path=proxy_path,
|
proxy_path=proxy_path,
|
||||||
managed=managed,
|
managed=managed,
|
||||||
category=manifest.tool.category if manifest.tool else None,
|
systemd=systemd_info,
|
||||||
version=manifest.tool.version if manifest.tool else None,
|
version=manifest.tool.version if manifest.tool else None,
|
||||||
tool_type=manifest.tool.tool_type.value if manifest.tool else None,
|
|
||||||
source=manifest.tool.source if manifest.tool else None,
|
source=manifest.tool.source if manifest.tool else None,
|
||||||
system_dependencies=manifest.tool.system_dependencies if manifest.tool else [],
|
system_dependencies=manifest.tool.system_dependencies if manifest.tool else [],
|
||||||
schedule=schedule,
|
schedule=schedule,
|
||||||
@@ -72,7 +94,7 @@ def list_components() -> list[ComponentSummary]:
|
|||||||
"""List all registered components."""
|
"""List all registered components."""
|
||||||
config = load_config(settings.castle_root)
|
config = load_config(settings.castle_root)
|
||||||
return [
|
return [
|
||||||
_summary_from_manifest(name, m)
|
_summary_from_manifest(name, m, config.root)
|
||||||
for name, m in config.components.items()
|
for name, m in config.components.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -87,7 +109,7 @@ def get_component(name: str) -> ComponentDetail:
|
|||||||
detail=f"Component '{name}' not found",
|
detail=f"Component '{name}' not found",
|
||||||
)
|
)
|
||||||
manifest = config.components[name]
|
manifest = config.components[name]
|
||||||
summary = _summary_from_manifest(name, manifest)
|
summary = _summary_from_manifest(name, manifest, config.root)
|
||||||
raw = manifest.model_dump(mode="json", exclude_none=True)
|
raw = manifest.model_dump(mode="json", exclude_none=True)
|
||||||
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
return ComponentDetail(**summary.model_dump(), manifest=raw)
|
||||||
|
|
||||||
|
|||||||
@@ -16,19 +16,27 @@ from dashboard_api.models import ToolCategory, ToolDetail, ToolSummary
|
|||||||
router = APIRouter(tags=["tools"])
|
router = APIRouter(tags=["tools"])
|
||||||
|
|
||||||
|
|
||||||
def _tool_summary(name: str, manifest: ComponentManifest) -> ToolSummary:
|
def _tool_summary(name: str, manifest: ComponentManifest, root: Path | None = None) -> ToolSummary:
|
||||||
"""Build a ToolSummary from a manifest that has a tool spec."""
|
"""Build a ToolSummary from a manifest that has a tool spec."""
|
||||||
t = manifest.tool
|
t = manifest.tool
|
||||||
assert t is not None
|
assert t is not None
|
||||||
installed = bool(manifest.install and manifest.install.path and manifest.install.path.enable)
|
installed = bool(manifest.install and manifest.install.path and manifest.install.path.enable)
|
||||||
|
|
||||||
|
# Infer runner from run block or source directory
|
||||||
|
runner = manifest.run.runner if manifest.run else None
|
||||||
|
if runner is None and t.source and root:
|
||||||
|
source_dir = root / t.source
|
||||||
|
if (source_dir / "pyproject.toml").exists():
|
||||||
|
runner = "python_uv_tool"
|
||||||
|
elif source_dir.is_file():
|
||||||
|
runner = "command"
|
||||||
|
|
||||||
return ToolSummary(
|
return ToolSummary(
|
||||||
id=name,
|
id=name,
|
||||||
description=manifest.description,
|
description=manifest.description,
|
||||||
category=t.category,
|
|
||||||
source=t.source,
|
source=t.source,
|
||||||
tool_type=t.tool_type.value,
|
|
||||||
version=t.version,
|
version=t.version,
|
||||||
runner=manifest.run.runner if manifest.run else None,
|
runner=runner,
|
||||||
system_dependencies=t.system_dependencies,
|
system_dependencies=t.system_dependencies,
|
||||||
installed=installed,
|
installed=installed,
|
||||||
)
|
)
|
||||||
@@ -38,7 +46,6 @@ def _find_md_for_tool(
|
|||||||
root: Path,
|
root: Path,
|
||||||
source: str,
|
source: str,
|
||||||
tool_name: str,
|
tool_name: str,
|
||||||
category: str | None = None,
|
|
||||||
) -> Path | None:
|
) -> Path | None:
|
||||||
"""Find the .md documentation file for a tool source path."""
|
"""Find the .md documentation file for a tool source path."""
|
||||||
source_path = root / source
|
source_path = root / source
|
||||||
@@ -48,8 +55,8 @@ def _find_md_for_tool(
|
|||||||
return md
|
return md
|
||||||
elif source_path.is_dir():
|
elif source_path.is_dir():
|
||||||
py_name = tool_name.replace("-", "_")
|
py_name = tool_name.replace("-", "_")
|
||||||
if category:
|
pkg_name = source_path.name
|
||||||
md = source_path / "src" / category / f"{py_name}.md"
|
md = source_path / "src" / pkg_name / f"{py_name}.md"
|
||||||
if md.exists():
|
if md.exists():
|
||||||
return md
|
return md
|
||||||
return None
|
return None
|
||||||
@@ -66,18 +73,23 @@ def _strip_frontmatter(content: str) -> str:
|
|||||||
|
|
||||||
@router.get("/tools", response_model=list[ToolCategory])
|
@router.get("/tools", response_model=list[ToolCategory])
|
||||||
def list_tools() -> list[ToolCategory]:
|
def list_tools() -> list[ToolCategory]:
|
||||||
"""List tools grouped by category."""
|
"""List tools grouped by source directory."""
|
||||||
config = load_config(settings.castle_root)
|
config = load_config(settings.castle_root)
|
||||||
tools = {k: v for k, v in config.components.items() if v.tool}
|
tools = {k: v for k, v in config.components.items() if v.tool}
|
||||||
|
|
||||||
by_category: dict[str, list[ToolSummary]] = {}
|
by_group: dict[str, list[ToolSummary]] = {}
|
||||||
for name, manifest in tools.items():
|
for name, manifest in tools.items():
|
||||||
cat = manifest.tool.category or "uncategorized" # type: ignore[union-attr]
|
t = manifest.tool
|
||||||
by_category.setdefault(cat, []).append(_tool_summary(name, manifest))
|
assert t is not None
|
||||||
|
if t.source:
|
||||||
|
group = Path(t.source).name
|
||||||
|
else:
|
||||||
|
group = "standalone"
|
||||||
|
by_group.setdefault(group, []).append(_tool_summary(name, manifest, config.root))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
ToolCategory(name=cat, tools=sorted(items, key=lambda t: t.id))
|
ToolCategory(name=group, tools=sorted(items, key=lambda t: t.id))
|
||||||
for cat, items in sorted(by_category.items())
|
for group, items in sorted(by_group.items())
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -99,11 +111,11 @@ def get_tool(name: str) -> ToolDetail:
|
|||||||
detail=f"'{name}' is not a tool",
|
detail=f"'{name}' is not a tool",
|
||||||
)
|
)
|
||||||
|
|
||||||
summary = _tool_summary(name, manifest)
|
summary = _tool_summary(name, manifest, config.root)
|
||||||
docs: str | None = None
|
docs: str | None = None
|
||||||
t = manifest.tool
|
t = manifest.tool
|
||||||
if t.source:
|
if t.source:
|
||||||
md_path = _find_md_for_tool(config.root, t.source, name, t.category)
|
md_path = _find_md_for_tool(config.root, t.source, name)
|
||||||
if md_path and md_path.exists():
|
if md_path and md_path.exists():
|
||||||
docs = _strip_frontmatter(md_path.read_text())
|
docs = _strip_frontmatter(md_path.read_text())
|
||||||
if not docs:
|
if not docs:
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
|||||||
"description": "Test tool",
|
"description": "Test tool",
|
||||||
"install": {"path": {"alias": "test-tool"}},
|
"install": {"path": {"alias": "test-tool"}},
|
||||||
"tool": {
|
"tool": {
|
||||||
"category": "document",
|
|
||||||
"source": "tools/document",
|
"source": "tools/document",
|
||||||
"system_dependencies": ["pandoc"],
|
"system_dependencies": ["pandoc"],
|
||||||
},
|
},
|
||||||
@@ -46,9 +45,8 @@ def castle_root(tmp_path: Path) -> Generator[Path, None, None]:
|
|||||||
"test-tool-2": {
|
"test-tool-2": {
|
||||||
"description": "Another test tool",
|
"description": "Another test tool",
|
||||||
"tool": {
|
"tool": {
|
||||||
"category": "utility",
|
"source": "tools/utility",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tool_type": "script",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class TestToolsList:
|
|||||||
"""GET /tools endpoint tests."""
|
"""GET /tools endpoint tests."""
|
||||||
|
|
||||||
def test_returns_grouped_tools(self, client: TestClient) -> None:
|
def test_returns_grouped_tools(self, client: TestClient) -> None:
|
||||||
"""Returns tools grouped by category."""
|
"""Returns tools grouped by source directory name."""
|
||||||
response = client.get("/tools")
|
response = client.get("/tools")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -17,8 +17,8 @@ class TestToolsList:
|
|||||||
assert "document" in names
|
assert "document" in names
|
||||||
assert "utility" in names
|
assert "utility" in names
|
||||||
|
|
||||||
def test_categories_sorted(self, client: TestClient) -> None:
|
def test_groups_sorted(self, client: TestClient) -> None:
|
||||||
"""Categories are sorted alphabetically."""
|
"""Groups are sorted alphabetically."""
|
||||||
response = client.get("/tools")
|
response = client.get("/tools")
|
||||||
data = response.json()
|
data = response.json()
|
||||||
names = [cat["name"] for cat in data]
|
names = [cat["name"] for cat in data]
|
||||||
@@ -28,27 +28,29 @@ class TestToolsList:
|
|||||||
"""Tool summary has expected fields."""
|
"""Tool summary has expected fields."""
|
||||||
response = client.get("/tools")
|
response = client.get("/tools")
|
||||||
data = response.json()
|
data = response.json()
|
||||||
doc_cat = next(c for c in data if c["name"] == "document")
|
doc_group = next(c for c in data if c["name"] == "document")
|
||||||
tool = next(t for t in doc_cat["tools"] if t["id"] == "test-tool")
|
tool = next(t for t in doc_group["tools"] if t["id"] == "test-tool")
|
||||||
assert tool["description"] == "Test tool"
|
assert tool["description"] == "Test tool"
|
||||||
assert tool["category"] == "document"
|
|
||||||
assert tool["source"] == "tools/document"
|
assert tool["source"] == "tools/document"
|
||||||
assert tool["system_dependencies"] == ["pandoc"]
|
assert tool["system_dependencies"] == ["pandoc"]
|
||||||
|
# tool_type and category should not be present
|
||||||
|
assert "tool_type" not in tool
|
||||||
|
assert "category" not in tool
|
||||||
|
|
||||||
def test_installed_flag(self, client: TestClient) -> None:
|
def test_installed_flag(self, client: TestClient) -> None:
|
||||||
"""Tool with install.path is marked as installed."""
|
"""Tool with install.path is marked as installed."""
|
||||||
response = client.get("/tools")
|
response = client.get("/tools")
|
||||||
data = response.json()
|
data = response.json()
|
||||||
doc_cat = next(c for c in data if c["name"] == "document")
|
doc_group = next(c for c in data if c["name"] == "document")
|
||||||
tool = next(t for t in doc_cat["tools"] if t["id"] == "test-tool")
|
tool = next(t for t in doc_group["tools"] if t["id"] == "test-tool")
|
||||||
assert tool["installed"] is True
|
assert tool["installed"] is True
|
||||||
|
|
||||||
def test_not_installed_flag(self, client: TestClient) -> None:
|
def test_not_installed_flag(self, client: TestClient) -> None:
|
||||||
"""Tool without install.path is not marked as installed."""
|
"""Tool without install.path is not marked as installed."""
|
||||||
response = client.get("/tools")
|
response = client.get("/tools")
|
||||||
data = response.json()
|
data = response.json()
|
||||||
util_cat = next(c for c in data if c["name"] == "utility")
|
util_group = next(c for c in data if c["name"] == "utility")
|
||||||
tool = next(t for t in util_cat["tools"] if t["id"] == "test-tool-2")
|
tool = next(t for t in util_group["tools"] if t["id"] == "test-tool-2")
|
||||||
assert tool["installed"] is False
|
assert tool["installed"] is False
|
||||||
|
|
||||||
def test_service_excluded(self, client: TestClient) -> None:
|
def test_service_excluded(self, client: TestClient) -> None:
|
||||||
@@ -68,12 +70,12 @@ class TestToolDetail:
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["id"] == "test-tool"
|
assert data["id"] == "test-tool"
|
||||||
assert data["category"] == "document"
|
assert data["source"] == "tools/document"
|
||||||
assert data["system_dependencies"] == ["pandoc"]
|
assert data["system_dependencies"] == ["pandoc"]
|
||||||
|
|
||||||
def test_docs_from_file(self, client: TestClient, castle_root: Path) -> None:
|
def test_docs_from_file(self, client: TestClient, castle_root: Path) -> None:
|
||||||
"""Reads documentation from .md file."""
|
"""Reads documentation from .md file."""
|
||||||
# Create doc file matching the source lookup
|
# Create doc file matching the source lookup (src/<dir_name>/<tool>.md)
|
||||||
doc_dir = castle_root / "tools" / "document" / "src" / "document"
|
doc_dir = castle_root / "tools" / "document" / "src" / "document"
|
||||||
doc_dir.mkdir(parents=True)
|
doc_dir.mkdir(parents=True)
|
||||||
(doc_dir / "test_tool.md").write_text(
|
(doc_dir / "test_tool.md").write_text(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link } from "react-router-dom"
|
|||||||
import { ArrowDown, ArrowUp, ArrowUpDown, Download, Play, RefreshCw, Square, Trash2 } from "lucide-react"
|
import { ArrowDown, ArrowUp, ArrowUpDown, Download, Play, RefreshCw, Square, Trash2 } from "lucide-react"
|
||||||
import type { ComponentSummary, HealthStatus } from "@/types"
|
import type { ComponentSummary, HealthStatus } from "@/types"
|
||||||
import { useServiceAction, useToolAction } from "@/services/api/hooks"
|
import { useServiceAction, useToolAction } from "@/services/api/hooks"
|
||||||
import { runnerLabel, toolTypeLabel } from "@/lib/labels"
|
import { runnerLabel } from "@/lib/labels"
|
||||||
import { HealthBadge } from "./HealthBadge"
|
import { HealthBadge } from "./HealthBadge"
|
||||||
import { RoleBadge } from "./RoleBadge"
|
import { RoleBadge } from "./RoleBadge"
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ interface ComponentTableProps {
|
|||||||
statuses: HealthStatus[]
|
statuses: HealthStatus[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortKey = "id" | "role" | "runner" | "tool_type" | "category" | "schedule" | "port" | "status"
|
type SortKey = "id" | "role" | "runner" | "schedule" | "port" | "status"
|
||||||
type SortDir = "asc" | "desc"
|
type SortDir = "asc" | "desc"
|
||||||
|
|
||||||
function statusRank(s: HealthStatus | undefined, installed: boolean | null): number {
|
function statusRank(s: HealthStatus | undefined, installed: boolean | null): number {
|
||||||
@@ -76,10 +76,6 @@ export function ComponentTable({ components, statuses }: ComponentTableProps) {
|
|||||||
return dir * (a.roles[0] ?? "").localeCompare(b.roles[0] ?? "")
|
return dir * (a.roles[0] ?? "").localeCompare(b.roles[0] ?? "")
|
||||||
case "runner":
|
case "runner":
|
||||||
return dir * (a.runner ?? "").localeCompare(b.runner ?? "")
|
return dir * (a.runner ?? "").localeCompare(b.runner ?? "")
|
||||||
case "tool_type":
|
|
||||||
return dir * (a.tool_type ?? "").localeCompare(b.tool_type ?? "")
|
|
||||||
case "category":
|
|
||||||
return dir * (a.category ?? "").localeCompare(b.category ?? "")
|
|
||||||
case "schedule":
|
case "schedule":
|
||||||
return dir * (a.schedule ?? "").localeCompare(b.schedule ?? "")
|
return dir * (a.schedule ?? "").localeCompare(b.schedule ?? "")
|
||||||
case "port":
|
case "port":
|
||||||
@@ -137,8 +133,6 @@ export function ComponentTable({ components, statuses }: ComponentTableProps) {
|
|||||||
<SortHeader label="Name" sortKey="id" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
<SortHeader label="Name" sortKey="id" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||||
<SortHeader label="Roles" sortKey="role" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
<SortHeader label="Roles" sortKey="role" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||||
<SortHeader label="Runner" sortKey="runner" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
<SortHeader label="Runner" sortKey="runner" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||||
<SortHeader label="Package" sortKey="tool_type" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
|
||||||
<SortHeader label="Category" sortKey="category" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
|
||||||
<SortHeader label="Schedule" sortKey="schedule" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
<SortHeader label="Schedule" sortKey="schedule" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||||
<SortHeader label="Port" sortKey="port" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
<SortHeader label="Port" sortKey="port" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||||
<SortHeader label="Status" sortKey="status" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
<SortHeader label="Status" sortKey="status" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||||
@@ -155,7 +149,7 @@ export function ComponentTable({ components, statuses }: ComponentTableProps) {
|
|||||||
))}
|
))}
|
||||||
{sorted.length === 0 && (
|
{sorted.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={9} className="px-3 py-6 text-center text-[var(--muted)]">
|
<td colSpan={7} className="px-3 py-6 text-center text-[var(--muted)]">
|
||||||
No components match.
|
No components match.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -228,6 +222,7 @@ function ComponentRow({
|
|||||||
<Link
|
<Link
|
||||||
to={`/${component.id}`}
|
to={`/${component.id}`}
|
||||||
className="font-medium hover:text-[var(--primary)] transition-colors"
|
className="font-medium hover:text-[var(--primary)] transition-colors"
|
||||||
|
title={component.systemd?.unit_path ?? undefined}
|
||||||
>
|
>
|
||||||
{component.id}
|
{component.id}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -247,12 +242,6 @@ function ComponentRow({
|
|||||||
<td className="px-3 py-2.5 text-[var(--muted)]">
|
<td className="px-3 py-2.5 text-[var(--muted)]">
|
||||||
{component.runner ? runnerLabel(component.runner) : "—"}
|
{component.runner ? runnerLabel(component.runner) : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2.5 text-[var(--muted)]">
|
|
||||||
{component.tool_type ? toolTypeLabel(component.tool_type) : "—"}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2.5 text-[var(--muted)]">
|
|
||||||
{component.category ?? "—"}
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-2.5 font-mono text-[var(--muted)]">
|
<td className="px-3 py-2.5 font-mono text-[var(--muted)]">
|
||||||
{component.schedule ?? "—"}
|
{component.schedule ?? "—"}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ export const RUNNER_LABELS: Record<string, string> = {
|
|||||||
remote: "Remote",
|
remote: "Remote",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TOOL_TYPE_LABELS: Record<string, string> = {
|
|
||||||
python_standalone: "Python package",
|
|
||||||
script: "Shell script",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ROLE_DESCRIPTIONS: Record<string, string> = {
|
export const ROLE_DESCRIPTIONS: Record<string, string> = {
|
||||||
service: "Exposes HTTP endpoints",
|
service: "Exposes HTTP endpoints",
|
||||||
tool: "CLI utility installed to PATH",
|
tool: "CLI utility installed to PATH",
|
||||||
@@ -25,7 +20,3 @@ export const ROLE_DESCRIPTIONS: Record<string, string> = {
|
|||||||
export function runnerLabel(runner: string): string {
|
export function runnerLabel(runner: string): string {
|
||||||
return RUNNER_LABELS[runner] ?? runner
|
return RUNNER_LABELS[runner] ?? runner
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toolTypeLabel(toolType: string): string {
|
|
||||||
return TOOL_TYPE_LABELS[toolType] ?? toolType
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ArrowLeft, Check, Play, RefreshCw, Square } from "lucide-react"
|
|||||||
import { useQueryClient } from "@tanstack/react-query"
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
import { apiClient } from "@/services/api/client"
|
import { apiClient } from "@/services/api/client"
|
||||||
import { useComponent, useStatus, useServiceAction, useEventStream, useToolDetail } from "@/services/api/hooks"
|
import { useComponent, useStatus, useServiceAction, useEventStream, useToolDetail } from "@/services/api/hooks"
|
||||||
import { runnerLabel, toolTypeLabel } from "@/lib/labels"
|
import { runnerLabel } from "@/lib/labels"
|
||||||
import { ComponentFields } from "@/components/ComponentFields"
|
import { ComponentFields } from "@/components/ComponentFields"
|
||||||
import { HealthBadge } from "@/components/HealthBadge"
|
import { HealthBadge } from "@/components/HealthBadge"
|
||||||
import { LogViewer } from "@/components/LogViewer"
|
import { LogViewer } from "@/components/LogViewer"
|
||||||
@@ -142,12 +142,6 @@ export function ComponentDetailPage() {
|
|||||||
How this tool is packaged and what it depends on.
|
How this tool is packaged and what it depends on.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
|
||||||
{toolDetail.category && (
|
|
||||||
<>
|
|
||||||
<span className="text-[var(--muted)]">Category</span>
|
|
||||||
<span>{toolDetail.category}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{toolDetail.source && (
|
{toolDetail.source && (
|
||||||
<>
|
<>
|
||||||
<span className="text-[var(--muted)]">Source</span>
|
<span className="text-[var(--muted)]">Source</span>
|
||||||
@@ -160,12 +154,6 @@ export function ComponentDetailPage() {
|
|||||||
<span>{toolDetail.version}</span>
|
<span>{toolDetail.version}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{toolDetail.tool_type && (
|
|
||||||
<>
|
|
||||||
<span className="text-[var(--muted)]">Type</span>
|
|
||||||
<span>{toolTypeLabel(toolDetail.tool_type)}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{toolDetail.runner && (
|
{toolDetail.runner && (
|
||||||
<>
|
<>
|
||||||
<span className="text-[var(--muted)]">Runner</span>
|
<span className="text-[var(--muted)]">Runner</span>
|
||||||
@@ -199,6 +187,26 @@ export function ComponentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{component.systemd && (
|
||||||
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
|
||||||
|
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-1">
|
||||||
|
Systemd
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mt-3">
|
||||||
|
<span className="text-[var(--muted)]">Unit</span>
|
||||||
|
<span className="font-mono">{component.systemd.unit_name}</span>
|
||||||
|
<span className="text-[var(--muted)]">Path</span>
|
||||||
|
<span className="font-mono">{component.systemd.unit_path}</span>
|
||||||
|
{component.systemd.timer && (
|
||||||
|
<>
|
||||||
|
<span className="text-[var(--muted)]">Timer</span>
|
||||||
|
<span>Active</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
|
||||||
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-4">
|
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-4">
|
||||||
Configuration
|
Configuration
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
export interface SystemdInfo {
|
||||||
|
unit_name: string
|
||||||
|
unit_path: string
|
||||||
|
timer: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ComponentSummary {
|
export interface ComponentSummary {
|
||||||
id: string
|
id: string
|
||||||
description: string | null
|
description: string | null
|
||||||
@@ -7,9 +13,8 @@ export interface ComponentSummary {
|
|||||||
health_path: string | null
|
health_path: string | null
|
||||||
proxy_path: string | null
|
proxy_path: string | null
|
||||||
managed: boolean
|
managed: boolean
|
||||||
category: string | null
|
systemd: SystemdInfo | null
|
||||||
version: string | null
|
version: string | null
|
||||||
tool_type: string | null
|
|
||||||
source: string | null
|
source: string | null
|
||||||
system_dependencies: string[]
|
system_dependencies: string[]
|
||||||
schedule: string | null
|
schedule: string | null
|
||||||
@@ -57,9 +62,7 @@ export interface SSEServiceActionEvent {
|
|||||||
export interface ToolSummary {
|
export interface ToolSummary {
|
||||||
id: string
|
id: string
|
||||||
description: string | null
|
description: string | null
|
||||||
category: string | null
|
|
||||||
source: string | null
|
source: string | null
|
||||||
tool_type: string | null
|
|
||||||
version: string | null
|
version: string | null
|
||||||
runner: string | null
|
runner: string | null
|
||||||
system_dependencies: string[]
|
system_dependencies: string[]
|
||||||
|
|||||||
@@ -132,8 +132,6 @@ Creates a shim so the tool is available system-wide after
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
tool:
|
tool:
|
||||||
tool_type: python_standalone # or "script"
|
|
||||||
category: document # Grouping for display
|
|
||||||
source: tools/document/ # Source directory
|
source: tools/document/ # Source directory
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
system_dependencies: [pandoc, poppler-utils]
|
system_dependencies: [pandoc, poppler-utils]
|
||||||
@@ -143,6 +141,11 @@ This block provides metadata for `castle tool list` and the dashboard.
|
|||||||
It's separate from `install` (which handles PATH registration) and `run`
|
It's separate from `install` (which handles PATH registration) and `run`
|
||||||
(which handles execution).
|
(which handles execution).
|
||||||
|
|
||||||
|
The install method (uv tool install vs symlink) is inferred from the source
|
||||||
|
directory: if `pyproject.toml` exists, it's a Python package; if the source
|
||||||
|
is a file, it's symlinked. Grouping for `castle tool list` and the dashboard
|
||||||
|
is derived from the source directory name (e.g., `tools/document/` → "document").
|
||||||
|
|
||||||
### `build` — How to build it
|
### `build` — How to build it
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -223,8 +226,6 @@ components:
|
|||||||
my-tool:
|
my-tool:
|
||||||
description: Does something useful
|
description: Does something useful
|
||||||
tool:
|
tool:
|
||||||
tool_type: python_standalone
|
|
||||||
category: utilities
|
|
||||||
source: my-tool/
|
source: my-tool/
|
||||||
install:
|
install:
|
||||||
path:
|
path:
|
||||||
|
|||||||
@@ -375,8 +375,6 @@ components:
|
|||||||
my-tool:
|
my-tool:
|
||||||
description: Does something useful
|
description: Does something useful
|
||||||
tool:
|
tool:
|
||||||
tool_type: python_standalone
|
|
||||||
category: utilities
|
|
||||||
source: my-tool/
|
source: my-tool/
|
||||||
install:
|
install:
|
||||||
path:
|
path:
|
||||||
@@ -390,8 +388,6 @@ components:
|
|||||||
pdf2md:
|
pdf2md:
|
||||||
description: Convert PDF files to Markdown
|
description: Convert PDF files to Markdown
|
||||||
tool:
|
tool:
|
||||||
tool_type: python_standalone
|
|
||||||
category: document
|
|
||||||
source: tools/document/
|
source: tools/document/
|
||||||
system_dependencies: [pandoc, poppler-utils]
|
system_dependencies: [pandoc, poppler-utils]
|
||||||
install:
|
install:
|
||||||
|
|||||||
Reference in New Issue
Block a user