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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ WORKING_DIR=/workspace
GITHUB_USER=
GITHUB_TOKEN=
TWINE_USERNAME=__token__
TWINE_PASSWORD=
TWINE_PASSWORD=
SPLENT_API_URL=http://host.docker.internal:5000
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.venv
.env
splent_cli.egg-info
venv
dist
Expand Down
42 changes: 40 additions & 2 deletions src/splent_cli/commands/feature/feature_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import requests
import click
from splent_cli.services import context
from splent_cli.services.api_client import SplentAPIError, get_package_by_name
from splent_cli.utils.cache_utils import make_feature_readonly
from splent_cli.utils.feature_utils import normalize_namespace

Expand Down Expand Up @@ -69,6 +70,42 @@ def _parse_full_name(full_name: str):
return namespace, repo, version


def _feature_api_name(repo: str) -> str:
if repo.startswith("splent_feature_"):
return repo
return f"splent_feature_{repo}"


def _resolve_full_name_from_api(full_name: str) -> str:
raw = full_name
version = None
if "@" in raw:
raw, version = raw.split("@", 1)


repo = raw.split("/")[-1]
api_name = _feature_api_name(repo)

try:
package = get_package_by_name(api_name)
except SplentAPIError as exc:
click.secho(f"❌ {exc}", fg="red")
click.echo(" Check SPLENT_API_URL or start the package index.")
raise SystemExit(1) from exc

if not isinstance(package, dict):
click.secho("❌ Invalid package response from API.", fg="red")
raise SystemExit(1)

resolved = package.get("full_name")
if not isinstance(resolved, str) or "/" not in resolved:
resolved = f"splent-io/{package.get('name') or api_name}"

if version:
return f"{resolved}@{version}"
return resolved


def _validate_identifier_part(value: str, label: str):
if not re.fullmatch(r"[a-zA-Z0-9_\-\.]+", value):
raise SystemExit(
Expand All @@ -84,17 +121,18 @@ def _validate_identifier_part(value: str, label: str):
@click.command(
"feature:clone",
short_help="Clone a feature into the local cache.",
help="Clone a feature into the local cache namespace.",
help="Clone a marketplace feature into the local cache.",
)
@click.argument("full_name", required=True)
def feature_clone(full_name):
"""
Clone <namespace>/<repo> into:
Clone <feature>, <repo> or <namespace>/<repo> into:
.splent_cache/features/<namespace>/<repo>@<version>

- If version is omitted, fetches the latest tag or 'main'.
"""

full_name = _resolve_full_name_from_api(full_name)
namespace, repo, version = _parse_full_name(full_name)

_validate_identifier_part(namespace, "namespace")
Expand Down
135 changes: 135 additions & 0 deletions src/splent_cli/commands/feature/feature_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import shutil
import textwrap

import click

from splent_cli.services.api_client import SplentAPIError, get_package_by_name


def _contract_description(package: dict) -> str:
contract = package.get("contract") or {}
return contract.get("description") or ""


def _contract_items(package: dict, key: str) -> list[str]:
contract = package.get("contract") or {}
values = contract.get(key) or {}

if isinstance(values, dict):
items = []
for name, value in sorted(values.items()):
if isinstance(value, list):
if value:
items.append(f"{name}: {', '.join(str(item) for item in value)}")
elif value:
items.append(f"{name}: {value}")
return items
if isinstance(values, list):
return sorted(str(name) for name in values)
if isinstance(values, str):
return [values]
return []


def _terminal_width() -> int:
return min(shutil.get_terminal_size((100, 20)).columns, 120)


def _echo_wrapped(label: str, value: str, width: int) -> None:
prefix = f" {label:<12}"
wrapped = textwrap.wrap(
value or "-",
width=max(width - len(prefix), 30),
subsequent_indent=" " * len(prefix),
)
if not wrapped:
click.echo(prefix + "-")
return

click.echo(prefix + wrapped[0])
for line in wrapped[1:]:
click.echo(line)


def _echo_contract_section(label: str, items: list[str], width: int) -> None:
click.echo(f" {label}")
if not items:
click.echo(" -")
return

for item in items:
wrapped = textwrap.wrap(
item,
width=max(width - 6, 30),
initial_indent=" - ",
subsequent_indent=" ",
)
for line in wrapped:
click.echo(line)


def _repo_url(package: dict) -> str | None:
return package.get("repo_url") or None


def _updated_at(package: dict) -> str:
metadata = package.get("metadata") or {}
value = metadata.get("updated_at") or package.get("updated_at") or ""
if "T" in value:
return value.split("T", 1)[0]
return value or "-"


def _feature_api_name(feature_name: str) -> str:
if feature_name.startswith("splent_feature_"):
return feature_name
return f"splent_feature_{feature_name}"


@click.command("feature:info", short_help="Show marketplace feature details.")
@click.argument("feature_name", required=True)
def feature_info(feature_name):
"""
Show marketplace information for one feature.

\b
Examples:
splent feature:info auth
splent feature:info splent_feature_auth
"""
api_name = _feature_api_name(feature_name)
click.echo(click.style(f"\n Loading feature {api_name}...\n", fg="cyan"))

try:
package = get_package_by_name(api_name)
except SplentAPIError as exc:
click.secho(f"❌ {exc}", fg="red")
click.echo(" Check SPLENT_API_URL or start the package index.")
raise SystemExit(1)

if not isinstance(package, dict):
click.secho("❌ Invalid package response from API.", fg="red")
raise SystemExit(1)

width = _terminal_width()
name = package.get("name") or api_name
desc = _contract_description(package)
updated = _updated_at(package)
provides = _contract_items(package, "provides")
requires = _contract_items(package, "requires")

click.secho(f" {name}", bold=True)
_echo_wrapped("full name", package.get("full_name") or "-", width)
_echo_wrapped("updated", updated, width)
_echo_wrapped("summary", desc, width)
_echo_contract_section("provides", provides, width)
_echo_contract_section("requires", requires, width)

url = _repo_url(package)
if url:
_echo_wrapped("repo", url, width)

click.echo()


cli_command = feature_info
34 changes: 34 additions & 0 deletions src/splent_cli/commands/feature/feature_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import click
import requests
from splent_cli.services import context, compose
from splent_cli.services.api_client import SplentAPIError, get_package_by_name
from splent_cli.utils.feature_utils import read_features_from_data


Expand Down Expand Up @@ -169,6 +170,39 @@ def feature_install(feature_identifier, env_scope, mode, version):
namespace, namespace_github, namespace_fs, feature_name = (
compose.parse_feature_identifier(feature_identifier)
)

api_feature_name = (
feature_name
if feature_name.startswith("splent_feature_")
else f"splent_feature_{feature_name}"
)
try:
package = get_package_by_name(api_feature_name)
except SplentAPIError as exc:
click.secho(f"❌ {exc}", fg="red")
click.echo(" Check SPLENT_API_URL or start the package index.")
raise SystemExit(1)

if not isinstance(package, dict):
click.secho("❌ Invalid package response from API.", fg="red")
raise SystemExit(1)

package_name = package.get("name") or api_feature_name
full_name = package.get("full_name")
if isinstance(full_name, str) and "/" in full_name:
feature_identifier, _, package_version = full_name.partition("@")
if package_version and not version:
version = package_version
else:
repository = package.get("repository")
if isinstance(repository, str) and "/" in repository:
feature_identifier = repository
else:
owner = package.get("owner") or namespace_github
feature_identifier = f"{owner}/{package_name}"
namespace, namespace_github, namespace_fs, feature_name = (
compose.parse_feature_identifier(feature_identifier)
)
short = feature_name.replace("splent_feature_", "")

# ── Ask mode if not specified ─────────────────────────────────────
Expand Down
Loading